基于 Cloudflare 的 Emby 影视展示站改造:从公网发布到访问体验优化

1 从媒体服务器到影视展示站

对于家里收藏了大量影视资源的人而言,估计心里或多或少都会有把收藏的资源展示给大家看一看的念头——当然,这里的“看一看”只是单纯地看看海报、看看收藏规模、看看都收藏了哪些影片,而非真正提供在线播放服务。毕竟播放不仅会持续占用家庭宽带有限的上行带宽(现在国内运营商对家庭宽带上行流量的限制非常严,流量稍大一点就可能触发限速甚至封禁),同时,将影视资源公开提供播放服务本身也是一个比较敏感的话题,并不适合作为个人爱好项目长期运行。

因此,很多时候像我一样的收藏者,需求其实很简单:把自己的影视收藏做成一个可以公开访问的展示站。 让别人能够像浏览豆瓣片单一样,看看自己收藏了什么电影、什么电视剧,以及整个收藏体系的大致规模。

从技术角度来说,实现这一目标似乎并不困难。目前比较流行的媒体管理系统,例如 Emby、Jellyfin 和 Plex,本身就提供了成熟的 Web 界面,能够自动抓取海报、演员信息、剧情简介以及各种媒体元数据。从表面上看,只需要把其中一个系统发布到公网,问题似乎就解决了。

但实际操作之后就会发现,事情并没有这么简单。因为这些系统虽然都提供了 Web 界面,但它们的设计目标本质上仍然是为家庭内部用户提供媒体管理与播放服务。换句话说,它们首先是“媒体服务器”,其次才是“网页应用”。

而当我们试图把它们直接变成一个面向互联网访客的影视展示站时,就会出现各种各样的“水土不服”。

例如:

  • 访客只是想浏览海报,却仍然需要经过完整的登录流程;
  • 原本在局域网环境下几乎感觉不到的资源加载延迟,在公网环境下会被明显放大,尤其是海报图片和前端静态资源数量较多时,整体访问体验会出现明显下降。
  • 登录页面首次加载往往存在明显延迟,公网访问时尤其明显,对普通访客而言,十几秒的等待很容易让人误以为网站已经失效;

究其原因,并不是这些系统设计得不好,而是它们从诞生之初就主要服务于家庭媒体管理场景。当使用场景从“家庭内部使用”变成“公网访客浏览”之后,一些原本并不起眼的问题便会被迅速放大。

换句话说,对于一个影视展示站而言,能够被公网访问,与适合被公网访问,其实是两件完全不同的事情。如果希望将现成的媒体管理系统打造成一个真正适合公网访问的影视展示站,仅仅完成公网发布还远远不够,还需要围绕访问方式、认证流程以及用户体验等多个方面进行针对性的调整和优化。

本文将以 Emby 为例,记录我是如何一步一步解决这些问题,并最终将其改造成一个适合公网访问的影视展示站的。

2 前置准备:使用 Cloudflare Tunnel 发布 Emby

在解决登录体验、访问体验等问题之前,首先需要解决一个最基础的问题:如何让家里的 Emby 能够被互联网访问。 对于绝大多数家庭影音爱好者而言,Emby 通常都部署在家庭网络内部,运行在 NAS、小主机或者家庭服务器之上,此时所有访问请求都来自局域网,因此能够稳定运行。

而一旦希望把它改造成一个公网可访问的影视展示站,就必须先解决一个问题:如何让互联网用户能够访问到家里的 Emby 服务。

最直接的方案自然是公网 IP 配合端口映射。如果家庭宽带拥有公网 IP 地址(无论是 IPv4 还是 IPv6),理论上只需要在路由器上配置相应的端口转发规则,就能将内网中的 Emby 发布到公网。这种方案在早期个人建站和家庭服务器场景中十分常见,配置简单直接,也几乎不需要额外成本。但放到今天,它已经逐渐不再适合作为家庭环境下的默认选择。

首先面临的,是公网 IP 的获取与长期可用性问题。随着 IPv4 地址资源日益紧张,越来越多运营商开始采用 CGNAT(运营商级 NAT)方案,许多家庭宽带实际上已经不具备真正意义上的公网 IPv4 地址。即使当前能够申请到公网 IPv4,也未必能够长期稳定保留。IPv6 虽然从根本上解决了地址枯竭的问题,但对于面向公众开放的网站来说,它同样存在一定局限性。因为访问者也必须具备可用的 IPv6 网络环境,才能正常访问站点。这意味着网站能否被顺利访问,不仅取决于服务端,还受到访问者网络条件的影响,从而提高了整体的访问门槛。

其次,即便公网 IP 的问题能够解决,直接将家庭网络暴露到互联网仍然会带来额外的运维成本。例如端口开放与路由配置需要自行维护,服务需要长期面对来自公网的扫描和探测,HTTPS 证书需要单独申请和管理,而真实公网 IP 也会直接暴露在外部网络中。对于一个主要承担展示功能的影视展示站来说,这些额外投入显得有些得不偿失。

此外,还有一个经常被忽略但十分现实的因素:近年来,国内运营商对于家庭宽带运行 Web 服务的态度普遍趋于谨慎。虽然不同地区、不同运营商的具体策略存在差异,但长期通过家庭宽带对外提供网站服务,本身就存在一定的不确定性。对于一个希望长期稳定运行的影视展示站而言,这显然不是一个理想的基础条件。

因为以上原因,对于个人搭建面向公众开放的影视展示站而言,我们更希望获得一种既不依赖公网 IPv4、又不受 IPv6 普及程度影响的发布方式,在这种背景下,我最终选择了另一种更适合家庭场景的方案:Cloudflare Tunnel。 它的关键区别不在于“多了一层代理”,而在于访问模型发生了变化——不再依赖入站端口,而是由家庭服务器主动向外建立一条持续连接通道,由 Cloudflare 负责转发外部访问请求。


Cloudflare Tunnel的简要配置可以参考文章:通过tunnel技术,让无公网IP的家庭宽带也能白嫖cloudflare实现快速建站(推荐),想进一步了解的朋友,可以参考文章:cloudflare教程(九) Zero Trust常用功能介绍及多场景使用教程


对于家庭环境而言,这种方式带来的最大变化是:公网访问能力不再依赖是否有公网 IP。换句话说,只要家庭服务器能够正常访问互联网,就能够稳定地向外提供服务。与此同时,端口暴露和公网扫描等问题也随之消失,家庭网络不再需要专门为公网访问去调整路由和开放端口。对于一个以展示为主的影视站点来说,这意味着整个系统的部署和维护都简单了许多。

除此之外,Cloudflare Tunnel 还顺带解决了 HTTPS 证书的申请与续期问题。而由于所有访问流量都会经过 Cloudflare 网络,后续无论是缓存优化、访问加速,还是访问流程上的一些改造,也都有了实现基础。

因此,在这个项目中,Cloudflare Tunnel 的作用并不仅仅是把 Emby 发布到公网这么简单。它实际上承担了整个影视展示站的统一入口,后面很多体验层面的优化,也都是建立在这一层基础之上完成的。

在完成公网接入之后,接下来真正的问题才开始出现:如何让一个默认面向登录用户设计的媒体系统,变成一个适合“只浏览、不操作”的影视展示站。

3 公网访问体验优化

3.1 新建一个独立的展示实例(可选但推荐)

在将 Emby 用作公网影视展示站时,一个容易被忽略但实际很关键的问题是:是否应该直接使用现有的生产媒体库对外提供访问。

从实现角度来看,这当然是最简单的方案,直接利用现有的媒体服务器就可以快速获得一个可用的展示入口。但如果从系统结构的角度去看,这种方式本质上是把“日常使用环境”和“公网展示环境”放在同一个实例中运行。两者虽然使用的是同一套媒体库,但目标完全不同:一个面向个人管理与播放,一个面向公开浏览与展示。

这种耦合在短期内通常不会带来明显问题,但在长期对公网开放的情况下,会逐渐引入一些隐性风险。例如任何配置调整都有可能影响展示效果,日常的维护操作也可能对外部访问产生干扰,而展示站本身也无法在一个稳定、可控的环境中独立演进。

因此,在更稳妥的实践中,一个更清晰的方式是单独部署一个用于展示的 Emby 实例(通常推荐使用 Docker 方式部署),将其与日常使用的媒体服务器进行逻辑或物理分离。这个展示实例可以采用共享媒体文件的方式,也可以仅同步部分媒体内容,其核心目的并不是扩展功能,而是划清“日常使用”与“对外展示”之间的边界。


如果个人收藏的影视资源太多,新建展示实例之后手动创建所有媒体库并重新扫描会耽搁大量时间(我仅仅扫描一个”欧美电影”库,加上刮削时间在内,起码需要1-2天),其实快速迁移媒体库数据也有技巧,详情可以参见文章:emby系列 高端:快速迁移新旧emby服务器的媒体库数据


同时,从安全角度来看,这个展示实例需要暴露在公网环境中,应当尽量避免使用默认或弱口令的管理员账号与密码组合,以降低被扫描或撞库的风险。

不过需要说明的是,这一步并不是必须的。这一节更准确的定位是一个推荐的架构隔离策略,而不是前置条件。

在完成这一层选择之后,才进入真正的访问体验优化问题,也就是如何让访客以最低成本进入系统本身。

3.2 Guest 账号:降低访问门槛

在将 Emby 用作公网影视展示站时,第一个需要解决的问题其实并不是性能,而是访问门槛本身:默认情况下,Emby 的访问模型仍然是以“用户登录”为中心设计的,任何访问都必须先完成账号认证(输入用户名/密码),才能进入媒体库页面。

但在影视展示站的使用场景下,大部分访客的行为非常明确:打开链接后浏览影视资源海报,而不是进入一个完整的媒体系统。因此,如果仍然保留完整的登录流程,本质上会让“看内容”这件事被不必要的步骤打断。

为了解决这个问题,我采用的方式是创建账号:”Guest/空密码”,并将其作为默认访问入口(只显示Guest账号,隐藏包括管理员账号在内的其他账号):

image.png

Guest 账号本身的权限也被严格限制,仅开放只读浏览能力,不包含播放权限与管理权限,确保其只作为展示入口使用。需要强调的是,这里使用Guest并不能“取消登录步骤”,而只是将认证过程简化到极致:用户进入站点后,只需要点击一次 Guest 登录入口,即可直接进入海报浏览界面,不需要输入账号密码,也不需要任何额外操作。


要在登录界面只显示Guest账号,需要在其他账号里进行隐藏设置:

image.png

同时,界面下方的提示”直接点击guest即可登录”,是在”设置”-“通用”里的”登录免责声明”里设置:
image.png


调整之后,访问路径从原本的:手动输入”用户名/密码”登录 → 进入系统 → 浏览内容变成了:点击 Guest 登录 → 进入海报墙 → 浏览内容。虽然本质上仍然存在认证步骤,但对于访客来说,这一步已经从“需要理解和输入信息的登录过程”,变成了“点击一下即可进入”的轻量操作。在实际体验中,这个差异非常关键,因为它直接决定了访客是否会在第一步就产生流失。

因此,从整个系统改造的角度来看,Guest 账号并不是一个功能优化,而是将 Emby 从“面向用户的媒体系统”转换为“面向浏览的展示系统”的第一步。

3.3 Cloudflare 缓存:优化资源加载

在通过 Guest 账号降低访问门槛之后,访客已经可以比较顺畅地进入 Emby 了。

不过新的问题很快又会出现,对于部署在家庭网络中的 Emby 而言,每一次公网访问实际上都需要跨越互联网到达家庭服务器,然后再返回结果。虽然单次请求的延迟未必很高,但 Emby 的 Web 页面本身会加载大量资源,包括前端静态文件、海报图片以及各种元数据接口。

在局域网环境下,这些请求几乎感觉不到延迟;而放到公网环境之后,大量请求叠加在一起,页面响应速度就会明显下降。

既然整个系统已经通过 Cloudflare Tunnel 接入了 Cloudflare,那么最自然的优化方式就是利用 Cloudflare 的边缘缓存能力,将那些变化频率较低的内容尽可能缓存到 Cloudflare 节点上,而不是每次都回源到家庭服务器。

在实际配置中,我主要使用的是 Cloudflare 的 Cache Rules 功能(不熟悉CF 缓存规则配置的朋友,可以参考之前的文章:cloudflare教程(六) CF Cache Rules(缓存规则)功能介绍及详细配置教程),并针对 Emby 的访问特征设计了一条缓存规则(以我的展示站的域名”movie.tangwudi.com”为例):

(http.host eq "movie.tangwudi.com" and http.request.uri.path contains "/Images/")
or
(http.host eq "movie.tangwudi.com" and starts_with(http.request.uri.path, "/web/"))
or
(http.host eq "movie.tangwudi.com" and http.request.uri.path contains "/emby/Users/")
or
(http.host eq "movie.tangwudi.com" and http.request.uri.path contains "/emby/Items")
or
(http.host eq "movie.tangwudi.com" and http.request.uri.path contains "/emby/System/")

对于满足上述条件的请求,直接启用 Cloudflare Cache,并将边缘缓存时间设置为 1 个月(最长时间可达 1 年)。

之所以选择这些路径,主要是因为它们对应的内容变化频率都比较低。其中:

  • /Images/ 主要包含电影海报、背景图以及演员头像等图片资源;
  • /web/ 对应 Emby Web 前端的静态文件,包括 JavaScript、CSS 和字体文件;
  • /emby/Users/ 包含用户相关信息,在展示场景下变化非常少;
  • /emby/Items 主要提供媒体库元数据,例如影片名称、简介、评分等信息;
  • /emby/System/ 则包含系统配置与基础状态信息。

对于一个以展示为主的影视站点来说,上述内容往往在很长时间内都不会发生明显变化,因此非常适合交由 Cloudflare 缓存。这样做之后,很多请求都可以直接在 Cloudflare 边缘节点完成响应,而无需回源到家庭服务器。

从用户角度来看,最直接的变化就是海报加载速度更快、页面切换更加流畅;而从服务器角度来看,则减少了大量重复请求对家庭网络和 Emby 实例造成的额外压力。

当然,需要注意的是,缓存并不适合所有接口。对于涉及实时状态、权限判断或者频繁变化的数据,仍然应该保持回源访问,否则可能导致显示内容与实际状态不一致。

不过对于影视展示站这种以浏览为主的场景来说,仅仅缓存上述几个路径,就已经能够获得比较明显的体验提升。

3.4 Worker 启动页:重构等待体验(可选但推荐)

其实,经过 3.1 到 3.3 之后,影视资源展示站已经可以正常运行了。不过,对于国内用户而言,仍然存在一个比较影响体验的问题:首页打开速度偏慢。

这并不是 Emby 本身的问题,而是整个访问链路共同作用的结果:用户的请求需要先经过 Cloudflare 网络,再通过 Tunnel 转发到家庭服务器,而 Emby Web 在首次打开时还需要加载前端资源、初始化页面并请求各种媒体库数据。

即使经过缓存优化之后,这个过程依然无法完全消除。尤其是在国内网络环境下,首次访问时出现数秒甚至十多秒左右的等待都不罕见。

从技术角度来说,这类问题并不好解决。因为它并不是某一个接口慢,而是整个访问链路和系统初始化过程叠加之后形成的结果。继续投入大量精力去优化,往往收益有限。

于是我换了一个思路:既然等待无法彻底消除,那么能不能让等待变得更容易接受一些?事实上,很多大型网站和应用在面对无法避免的加载过程时,采用的都不是单纯缩短时间,而是通过各种方式向用户反馈当前状态,让用户知道系统正在正常工作:例如安装程序会显示进度条,游戏启动时会显示加载画面,流媒体加载时的固定启动过场等,比如大家熟悉的netfix影片的片头”登登”:

image.png

本质上,它们解决的并不是性能问题,而是体验问题。

因此,我在 Emby 前面额外增加了一个 Cloudflare Worker 作为启动页(对CF worker功能不熟悉的朋友,可以参考我之前的文章:cloudflare教程(七) CF worker功能介绍及基于worker实现”乞丐版APO for WordPress”功能加速网站访问的实操、验证及相关技术原理研究),并将原本直接访问 Emby 的入口改成了一个新的访问地址:”movie.tangwudi.com/start” 。

当用户访问这个地址时,并不会立即进入 Emby,而是先看到一个简单的启动页面,页面会模拟一个影视资料库正在启动的过程,例如:

连接影视资料库...
建立安全连接...
验证媒体服务...
准备访问入口...
正在进入影视库...

同时配合进度条和动态动画效果,让用户能够感知到系统正在工作。经过数秒后,Worker 会自动跳转到真正的 Emby 登录页面。需要说明的是,这种方式并不会让 Emby 变得更快,从技术角度来看,它几乎没有优化任何性能指标。但从用户体验角度来看,两者的感受却完全不同。

原来的情况是:

点击链接
    ↓
有emby图标的页面漫长的等待
    ↓
怀疑网站是否失效
    ↓
出现登录界面
    ↓
进入 Emby

而现在的情况是:

点击链接
    ↓
看到启动页面
    ↓
知道系统正在加载
    ↓
出现登录界面
    ↓
进入 Emby

等待时间可能没有明显变化,但用户对于等待的接受程度却提高了很多。这也是这一优化方案最有意思的地方:它并没有试图解决一个难以解决的性能问题,而是通过重新设计等待过程,改善了用户对整个系统的感知。

当然,这一步并不是必须的。即使不增加 Worker 启动页,前面完成的优化已经足以支撑一个正常可用的影视展示站。

但如果你的主要访问者也是国内用户,并且比较在意首次访问时的体验,那么增加这样一个轻量级的启动层,往往能够带来比继续折腾性能参数更明显的改善效果。

我也把worker的js代码贴出来,感兴趣的朋友可以参考一下:

export default {
  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname !== "/start") {
      return fetch(request);
    }

    return new Response(`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>正在连接影视资料库</title>

<style>
html,body{
    margin:0;
    width:100%;
    height:100%;
    background:#050505;
    color:#ffffff;
    overflow:hidden;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
}

.container{
    height:100%;
    display:flex;
    flex-direction:column;
    justify-content:center;
    align-items:center;
    text-align:center;
}

.logo{
    font-size:40px;
    letter-spacing:6px;
    margin-bottom:40px;
    opacity:.95;
}

.status{
    font-size:16px;
    opacity:.85;
    margin-bottom:22px;
    min-height:24px;
}

.progress-wrap{
    width:320px;
    max-width:80vw;
}

.progress{
    height:8px;
    border-radius:999px;
    background:#1f1f1f;
    overflow:hidden;
}

.bar{
    width:0%;
    height:100%;
    background:#ffffff;
    transition:width .5s ease;
}

.percent{
    margin-top:12px;
    font-size:13px;
    opacity:.65;
}

.footer{
    position:absolute;
    bottom:40px;
    font-size:12px;
    opacity:.3;
    letter-spacing:2px;
}

.fadeout{
    animation:fadeout .8s forwards;
}

@keyframes fadeout{
    from{opacity:1;}
    to{opacity:0;}
}
</style>
</head>

<body>

<div class="container">
    <div class="logo">MOVIE LIBRARY</div>

    <div class="status" id="status">
        正在连接影视资料库...
    </div>

    <div class="progress-wrap">
        <div class="progress">
            <div class="bar" id="bar"></div>
        </div>
        <div class="percent" id="percent">0%</div>
    </div>
</div>

<div class="footer">
    TANGWUDI MOVIE COLLECTION
</div>

<script>
const statusEl = document.getElementById("status");
const bar = document.getElementById("bar");
const percent = document.getElementById("percent");

const steps = [
    { text: "连接影视资料库...", progress: 18 },
    { text: "建立安全连接...", progress: 38 },
    { text: "验证媒体服务...", progress: 62 },
    { text: "准备访问入口...", progress: 85 },
    { text: "正在进入影视库...", progress: 100 }
];

const totalTime = 6000 + Math.random() * 2000;

let index = 0;

function nextStep() {
    if (index >= steps.length) {
        document.body.classList.add("fadeout");

        setTimeout(() => {
            location.href =
                "/web/index.html#!/startup/login.html?serverId=d2f780f97e4d4f39928b1b1f97d3ca7f";
        }, 800);

        return;
    }

    const step = steps[index];

    statusEl.textContent = step.text;
    bar.style.width = step.progress + "%";
    percent.textContent = step.progress + "%";

    index++;

    setTimeout(nextStep, totalTime / steps.length);
}

nextStep();
</script>

</body>
</html>`, {
      headers: {
        "content-type": "text/html;charset=UTF-8",
        "cache-control": "no-store"
      }
    });
  }
};

最终效果大家可以直接访问以下链接体验:”https://movie.tangwudi.com/start

注:这个展示站我基本上一年更新一次,且只更新”欧美电影”、”亚洲电影”、”欧美怀旧电影”这3项,其他懒得更新了,太浪费时间了。

4 后话

虽然经过前面的改造之后,一个基于 Emby 的影视资源展示站已经基本成型,无论是公网发布、访问门槛还是加载体验,都已经能够满足日常展示需求。但如果从长期演进的角度来看,我并不认为这会是最终方案。

原因其实很简单,Emby 终究是一套媒体服务器。即便通过 Guest 账号、Cloudflare 缓存以及 Worker 启动页等方式解决了不少问题,本质上仍然是在围绕一套原本为媒体管理设计的系统进行适配。换句话说,前面所有优化实际上都在做同一件事:尽可能让 Emby 更适合作为一个影视展示站来使用。

但从另一个角度来看,一个真正为展示而设计的网站,其实不应该让Emby参与才对。对于展示站而言,我们真正需要的无非是影片名称、上映年份、评分、简介、演员信息、海报图片以及分类标签等元数据。而这些内容本身都已经存在于 Emby 的数据库中。理论上,只要能够通过 API 将这些数据导出出来,再配合海报图片等资源,就已经具备了构建一个独立展示站所需的全部基础条件。

因此,一个更加理想的架构或许应该是:Emby 只负责管理媒体库和维护元数据,而网站本身则完全静态化。网站运行时不再依赖 Emby,也不需要通过 Tunnel 回源到家庭服务器,所有页面、数据以及图片资源都直接部署到静态托管平台,由 CDN 负责分发。这样不仅访问速度会更快,整体架构也会简单得多。

事实上,在折腾这套展示站的过程中,我已经开始考虑这个方向,并且花了不少时间在互联网上寻找现成方案。但比较遗憾的是,无论是 Emby、Jellyfin 还是 Plex 社区,我几乎都没有找到成熟的静态展示项目。大部分讨论仍然集中在媒体服务器本身,而不是如何将整个媒体库导出为一个独立运行的静态网站。这也意味着,如果真的要往这个方向继续推进,最终大概率还是只能自己手搓了。

不过真正开始分析之后才发现,这件事并不像最初想象的那么简单。以我目前两万部电影的收藏规模为例,仅仅是生成包含影片信息的索引文件,就需要认真考虑数据结构设计的问题。海报图片如何同步、资源如何组织、数据如何增量更新、搜索功能如何实现、浏览器如何高效渲染数万条记录,这些都需要单独设计。

尤其是在前端展示层面,如果希望做到类似 Netflix 或者 Emby 海报墙那样的浏览体验,就必须考虑虚拟滚动等技术,否则浏览器很容易因为一次性渲染过多内容而出现性能问题。而搜索功能同样不能简单依赖后端接口,因为静态站点并不存在传统意义上的数据库查询能力,这意味着需要在浏览器端构建本地索引,通过 JavaScript 直接完成搜索。

这些问题单独看都不算特别复杂,但组合在一起之后,其实已经接近一个小型项目的规模了,我甚至觉得自己似乎被迫要去学习前端框架了~~~。因此,这件事情暂时还停留在规划阶段,并没有正式开始实施。

不过目前来看,本文介绍的方案已经足够满足一般朋友影视资源展示的需求,而静态化展示站更像是下一阶段准备慢慢填的一个坑。未来如果真的完成了这套系统,那么 Emby 在整个架构中的角色也许会发生变化——它不再是网站本身,而只是网站背后的数据源。

至于这个坑什么时候能够填完,我现在也说不好。不过对于个人项目而言,很多时候也并不需要一开始就拥有最终形态。先让它运行起来,先让它能够解决眼前的问题,然后再在实际使用过程中不断调整和演进,往往才是更符合现实的路线。

至少在静态化方案真正落地之前,本文介绍的这套方案已经足够让我把自己的影视收藏展示给更多人看了。

📌 内容结构提示:
这篇内容属于「Cloudflare 学习地图」的一部分,你可以从这里查看完整内容路径: Cloudflare 学习地图
分享这篇文章
博客内容均系原创,转载请注明出处!博客的RSS地址为:https://blog.tangwudi.com/feed,欢迎订阅;如有需要,可以加入Telegram群一起讨论问题。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇