Giter Club home page Giter Club logo

onelist's Introduction

onelist

一个类似emby的专注于刮削alist聚合网盘形成影视媒体库的程序。

主要解决以下痛点:

  • alist挂载云盘后能在网页端看视频,却没有分类,没有海报墙

  • 使用webdav挂载本地后,用jellyfin或者emby刮削会下载视频截取封面导致封号

  • 用jellyfin或者emby之类,没有大带宽公网ip,在外难以访问

常见问题汇总:

多种安装方式,推荐docker安装:


docker安装 | docker-compose方式安装


手动安装教程:https://www.bilibili.com/video/BV15M41177LN

1.程序下载

可以在github发布页下载已经编译好的二进制文件

使用前必看,程序采用themoviedb作为刮削的资源库,推荐使用国外主机,否则你需要修改hosts文件。

99.84.251.12 api.themoviedb.org
99.84.251.19 api.themoviedb.org
99.84.251.67 api.themoviedb.org
99.84.251.108 api.themoviedb.org
156.146.56.162 image.tmdb.org
108.138.246.49 image.tmdb.org

2.下载后先初始化配置文件

输入./onelist -run config命令,便会生成配置文件config.env 修改完config.env配置文件后,运行onelist -run server便可启动项目,运行onelist -run admin可查看管理员账户!

config.env

# 服务设置
# 注意要改为未被占用的端口
API_PORT=5245
FaviconicoUrl=https://wework.qpic.cn/wwpic/818353_fizV30xbQCGPQRP_1677394564/0
API_SECRET=fRVvjcNd11gYGI85StVaeCtPVSmJTRRE

# Env有两种模式,Debug及Release,主要用在数据库为mysql时候,需要注意修改Env环境和mysql密码对应
Env=Debug

# 管理员账户设置,用于初始化管理员账户
[email protected]
UserPassword=xxxxx

# 数据库设置
DB_DRIVER=sqlite
DB_USER=root
DbName=onelist

# 如果上面DB_DRIVER类型为mysql,就需要正确填下以下参数
DB_PASSWORD_Debug=123456
DB_PASSWORD_Release=123456

# TheMovieDb Key
# 在https://www.themoviedb.org网站申请
KeyDb=22f10ca52f109158ac7fe064ebbcf697

3.运行程序

# 先运行,查看有无错误
./onelist -run server

注意:如果提示权限问题,可以先授权文件chmod 777 onelist

# 如果想后台一直保持运行,可用以下命令
nohup ./onelist -run server >/dev/null 2>&1 &

4.登录

访问你的ip:端口就可以进入管理后台了(记得防火墙放行该端口)

5.添加媒体库

1.对应输入媒体库名字,比如电影,类型选择movie

2.封面图片可以暂时不填

3.填写alist相关信息,这个主要用于程序查询你alist中文件,根据文件名进行刮削

6.挂载资源,新建完毕后,添加挂载目录。

挂载的目录中文件必须满足下面这种命名方式

电影就按电影名称

电视同一部美剧,所有季可以分开或者放在不同子目录,但是文件名一定得满足以下格式
权力的游戏S01E01.mp4
权力的游戏S01E02.mp4
权力的游戏S01E03.mp4

填写比如/阿里2号/电影01组即可,可以选择是否自动刮削,用于你网盘有新文件,程序自动给你添加进影库,

点击创建后反应比较慢,是因为程序去遍历你的alist文件了,稍微等下

注意:添加挂载目录只能选择你建立媒体库中采用的alist相关目录,要与alist域名一致

7.创建后点击刷新就可以看到刮削进度了

可以进入错误文件中查看

交流群:

群名称: onelist QQ群 号: 765592050

感谢您的关注!开源不易,需要开发者们的不断努力和付出。如果您觉得我的项目对您有所帮助,希望能够支持我继续改进和维护这个项目,您可以考虑打赏我一杯咖啡的钱。 您的支持将是我继续前进的动力,让我能够更加专注地投入到开源社区中,让我的项目变得更加完善和有用。如果您决定打赏我,可以通过以下方式:

  • 给该项目点赞   给该项目点赞
  • 关注我的 Github   关注我的 Github
微信 支付宝
微信 支付宝

onelist's People

Contributors

ddsderek avatar ddsrem avatar msterzhang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

onelist's Issues

新增挂载时失败

docker部署,提交时提示以下报错信息
context deadline exceeded (Client.Timeout exceeded while awaiting headers)

用网页播放不了视频

不知道为什么不能浏览器网页在线播放(Chrome、Safari、edge浏览器都不行),一直重新连接,但是用app比如inna打开就能播放。

docker安装报错

manifest for docker.io/msterzhang/onelist:latest not found
是改仓库了吗

许多搜刮不到

可以借鉴nastools代码,用软连接搜刮,这样子就不影响原文件,而且可以自动改名搜刮。nastools改名搜刮部分优化了很多次,已经可以搜刮到大多数不同的命名格式。
思路:在数据库中新建一张表,用来记录,改名文件在alist中的链接与改过的名字的对应关系。
不建议直接在alist改名。
1.aliist现在用的阿里云盘 Open在同一 IP 在120分钟内请求10次,会出现 TooManyRequests
(例如在 保存/编辑 的时候算一次请求,查看文件看视频下载不算),如果在alist改,容易触发阿里云盘的风控
2.这样更灵活,可以用第三方的手动改名软件本地改名。

功能建议

增加一个自定义tmdbapi地址
可以使用clodflare反代tmdb api
下面这个代码可以反代api.themoviedb.org

// node_modules/reflare/dist/src/database/workers-kv.js
var WorkersKV = class {
  namespace;
  constructor(namespace) {
    this.namespace = namespace;
  }
  get = async (key) => {
    const value = await this.namespace.get(key, {
      type: "json",
      cacheTtl: 60
    });
    return value;
  };
  put = async (key, value) => {
    await this.namespace.put(key, JSON.stringify(value));
  };
  delete = async (key) => {
    await this.namespace.delete(key);
  };
};

// node_modules/reflare/dist/src/middleware.js
var usePipeline = (...initMiddlewares) => {
  const stack = [...initMiddlewares];
  const push = (...middlewares) => {
    stack.push(...middlewares);
  };
  const execute = async (context) => {
    const runner = async (prevIndex, index) => {
      if (index === prevIndex) {
        throw new Error("next() called multiple times");
      }
      if (index >= stack.length) {
        return;
      }
      const middleware = stack[index];
      const next = async () => runner(index, index + 1);
      await middleware(context, next);
    };
    await runner(-1, 0);
  };
  return {
    push,
    execute
  };
};

// node_modules/reflare/dist/src/middlewares/cors.js
var useCORS = async (context, next) => {
  await next();
  const { request, response, route } = context;
  const corsOptions = route.cors;
  if (corsOptions === void 0) {
    return;
  }
  const { origin, methods, exposedHeaders, allowedHeaders, credentials, maxAge } = corsOptions;
  const requestOrigin = request.headers.get("origin");
  if (requestOrigin === null || origin === false) {
    return;
  }
  const corsHeaders = new Headers(response.headers);
  if (origin === true) {
    corsHeaders.set("Access-Control-Allow-Origin", requestOrigin);
  } else if (Array.isArray(origin)) {
    if (origin.includes(requestOrigin)) {
      corsHeaders.set("Access-Control-Allow-Origin", requestOrigin);
    }
  } else if (origin === "*") {
    corsHeaders.set("Access-Control-Allow-Origin", "*");
  }
  if (Array.isArray(methods)) {
    corsHeaders.set("Access-Control-Allow-Methods", methods.join(","));
  } else if (methods === "*") {
    corsHeaders.set("Access-Control-Allow-Methods", "*");
  } else {
    const requestMethod = request.headers.get("Access-Control-Request-Method");
    if (requestMethod !== null) {
      corsHeaders.set("Access-Control-Allow-Methods", requestMethod);
    }
  }
  if (Array.isArray(exposedHeaders)) {
    corsHeaders.set("Access-Control-Expose-Headers", exposedHeaders.join(","));
  } else if (exposedHeaders === "*") {
    corsHeaders.set("Access-Control-Expose-Headers", "*");
  }
  if (Array.isArray(allowedHeaders)) {
    corsHeaders.set("Access-Control-Allow-Headers", allowedHeaders.join(","));
  } else if (allowedHeaders === "*") {
    corsHeaders.set("Access-Control-Allow-Headers", "*");
  } else {
    const requestHeaders = request.headers.get("Access-Control-Request-Headers");
    if (requestHeaders !== null) {
      corsHeaders.set("Access-Control-Allow-Headers", requestHeaders);
    }
  }
  if (credentials === true) {
    corsHeaders.set("Access-Control-Allow-Credentials", "true");
  }
  if (maxAge !== void 0 && Number.isInteger(maxAge)) {
    corsHeaders.set("Access-Control-Max-Age", maxAge.toString());
  }
  context.response = new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: corsHeaders
  });
};

// node_modules/reflare/dist/src/middlewares/firewall.js
var fields = /* @__PURE__ */ new Set([
  "country",
  "continent",
  "asn",
  "ip",
  "hostname",
  "user-agent"
]);
var operators = /* @__PURE__ */ new Set([
  "equal",
  "not equal",
  "greater",
  "less",
  "in",
  "not in",
  "contain",
  "not contain",
  "match",
  "not match"
]);
var validateFirewall = ({ field, operator, value }) => {
  if (field === void 0 || operator === void 0 || value === void 0) {
    throw new Error("Invalid 'firewall' field in the option object");
  }
  if (fields.has(field) === false) {
    throw new Error("Invalid 'firewall' field in the option object");
  }
  if (operators.has(operator) === false) {
    throw new Error("Invalid 'firewall' field in the option object");
  }
};
var getFieldParam = (request, field) => {
  const cfProperties = request.cf;
  switch (field) {
    case "asn":
      return cfProperties?.asn;
    case "continent":
      return cfProperties?.continent;
    case "country":
      return cfProperties?.country;
    case "hostname":
      return request.headers.get("host") || "";
    case "ip":
      return request.headers.get("cf-connecting-ip") || "";
    case "user-agent":
      return request.headers.get("user-agent") || "";
    default:
      return void 0;
  }
};
var matchOperator = (fieldParam, value) => {
  if (!(value instanceof RegExp)) {
    throw new Error("You must use 'new RegExp('...')' for 'value' in firewall configuration to use 'match' or 'not match' operator");
  }
  return value.test(fieldParam.toString());
};
var notMatchOperator = (fieldParam, value) => !matchOperator(fieldParam, value);
var equalOperator = (fieldParam, value) => fieldParam === value;
var notEqualOperator = (fieldParam, value) => fieldParam !== value;
var greaterOperator = (fieldParam, value) => {
  if (typeof fieldParam !== "number" || typeof value !== "number") {
    throw new Error("You must use number for 'value' in firewall configuration to use 'greater' or 'less' operator");
  }
  return fieldParam > value;
};
var lessOperator = (fieldParam, value) => {
  if (typeof fieldParam !== "number" || typeof value !== "number") {
    throw new Error("You must use number for 'value' in firewall configuration to use 'greater' or 'less' operator");
  }
  return fieldParam < value;
};
var containOperator = (fieldParam, value) => {
  if (typeof fieldParam !== "string" || typeof value !== "string") {
    throw new Error("You must use string for 'value' in firewall configuration to use 'contain' or 'not contain' operator");
  }
  return fieldParam.includes(value);
};
var notContainOperator = (fieldParam, value) => !containOperator(fieldParam, value);
var inOperator = (fieldParam, value) => {
  if (!Array.isArray(value)) {
    throw new Error("You must use an Array for 'value' in firewall configuration to use 'in' or 'not in' operator");
  }
  return value.some((item) => item === fieldParam);
};
var notInOperator = (fieldParam, value) => !inOperator(fieldParam, value);
var operatorsMap = {
  match: matchOperator,
  contain: containOperator,
  equal: equalOperator,
  in: inOperator,
  greater: greaterOperator,
  less: lessOperator,
  "not match": notMatchOperator,
  "not contain": notContainOperator,
  "not equal": notEqualOperator,
  "not in": notInOperator
};
var useFirewall = async (context, next) => {
  const { request, route } = context;
  if (route.firewall === void 0) {
    await next();
    return;
  }
  route.firewall.forEach(validateFirewall);
  for (const { field, operator, value } of route.firewall) {
    const fieldParam = getFieldParam(request, field);
    if (fieldParam !== void 0 && operatorsMap[operator](fieldParam, value)) {
      throw new Error("You don't have permission to access this service.");
    }
  }
  await next();
};

// node_modules/reflare/dist/src/middlewares/headers.js
var setForwardedHeaders = (headers) => {
  headers.set("X-Forwarded-Proto", "https");
  const host = headers.get("Host");
  if (host !== null) {
    headers.set("X-Forwarded-Host", host);
  }
  const ip = headers.get("cf-connecting-ip");
  const forwardedForHeader = headers.get("X-Forwarded-For");
  if (ip !== null && forwardedForHeader === null) {
    headers.set("X-Forwarded-For", ip);
  }
};
var useHeaders = async (context, next) => {
  const { request, route } = context;
  const requestHeaders = new Headers(request.headers);
  setForwardedHeaders(requestHeaders);
  if (route.headers === void 0) {
    context.request = new Request(request.url, {
      body: request.body,
      method: request.method,
      headers: requestHeaders
    });
    await next();
    return;
  }
  if (route.headers.request !== void 0) {
    for (const [key, value] of Object.entries(route.headers.request)) {
      if (value.length === 0) {
        requestHeaders.delete(key);
      } else {
        requestHeaders.set(key, value);
      }
    }
  }
  context.request = new Request(request.url, {
    body: request.body,
    method: request.method,
    headers: requestHeaders
  });
  await next();
  const { response } = context;
  const responseHeaders = new Headers(response.headers);
  if (route.headers.response !== void 0) {
    for (const [key, value] of Object.entries(route.headers.response)) {
      if (value.length === 0) {
        responseHeaders.delete(key);
      } else {
        responseHeaders.set(key, value);
      }
    }
  }
  context.response = new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: responseHeaders
  });
};

// node_modules/reflare/dist/src/utils.js
var getHostname = (request) => {
  const url = new URL(request.url);
  return url.host;
};
var castToIterable = (value) => Array.isArray(value) ? value : [value];

// node_modules/reflare/dist/src/middlewares/load-balancing.js
var validateUpstream = (upstream) => {
  if (upstream.domain === void 0) {
    throw new Error("Invalid 'upstream' field in the option object");
  }
};
var ipHashHandler = (upstream, request) => {
  const ipString = request.headers.get("cf-connecting-ip") || "0.0.0.0";
  const userIP = ipString.split(".").map((octet, index, array) => parseInt(octet, 10) * 256 ** (array.length - index - 1)).reduce((accumulator, current) => accumulator + current);
  return upstream[userIP % upstream.length];
};
var randomHandler = (upstream) => {
  const weights = upstream.map((option) => option.weight === void 0 ? 1 : option.weight);
  const totalWeight = weights.reduce((acc, num, index) => {
    const sum = acc + num;
    weights[index] = sum;
    return sum;
  });
  if (totalWeight === 0) {
    throw new Error("Total weights should be greater than 0.");
  }
  const random = Math.random() * totalWeight;
  for (const index of weights.keys()) {
    if (weights[index] >= random) {
      return upstream[index];
    }
  }
  return upstream[Math.floor(Math.random() * upstream.length)];
};
var handlersMap = {
  random: randomHandler,
  "ip-hash": ipHashHandler
};
var useLoadBalancing = async (context, next) => {
  const { request, route } = context;
  const { upstream, loadBalancing } = route;
  if (upstream === void 0) {
    throw new Error("The required 'upstream' field in the option object is missing");
  }
  const upstreamIterable = castToIterable(upstream);
  upstreamIterable.forEach(validateUpstream);
  if (loadBalancing === void 0) {
    context.upstream = randomHandler(upstreamIterable, request);
    await next();
    return;
  }
  const policy = loadBalancing.policy || "random";
  const policyHandler = handlersMap[policy];
  context.upstream = policyHandler(upstreamIterable, request);
  await next();
};

// node_modules/reflare/dist/src/middlewares/upstream.js
var rewriteURL = (url, upstream) => {
  const cloneURL = new URL(url);
  const { domain, port, protocol } = upstream;
  cloneURL.hostname = domain;
  if (protocol !== void 0) {
    cloneURL.protocol = `${protocol}:`;
  }
  if (port === void 0) {
    cloneURL.port = "";
  } else {
    cloneURL.port = port.toString();
  }
  return cloneURL.href;
};
var useUpstream = async (context, next) => {
  const { request, upstream } = context;
  if (upstream === null) {
    await next();
    return;
  }
  const url = rewriteURL(request.url, upstream);
  context.request = new Request(url, context.request);
  if (upstream.onRequest) {
    const onRequest = castToIterable(upstream.onRequest);
    context.request = onRequest.reduce((reducedRequest, fn) => fn(reducedRequest, url), request);
  }
  context.response = (await fetch(context.request)).clone();
  if (upstream.onResponse) {
    const onResponse = castToIterable(upstream.onResponse);
    context.response = onResponse.reduce((reducedResponse, fn) => fn(reducedResponse, url), context.response);
  }
  await next();
};

// node_modules/reflare/dist/src/index.js
var filter = (request, routeList) => {
  const url = new URL(request.url);
  for (const route of routeList) {
    if (route.methods === void 0 || route.methods.includes(request.method)) {
      const match = castToIterable(route.path).some((path) => {
        const re = RegExp(`^${path.replace(/(\/?)\*/g, "($1.*)?").replace(/\/$/, "").replace(/:(\w+)(\?)?(\.)?/g, "$2(?<$1>[^/]+)$2$3").replace(/\.(?=[\w(])/, "\\.").replace(/\)\.\?\(([^[]+)\[\^/g, "?)\\.?($1(?<=\\.)[^\\.")}/*$`);
        return url.pathname.match(re);
      });
      if (match) {
        return route;
      }
    }
  }
  return void 0;
};
var defaultOptions = {
  provider: "static",
  routeList: []
};
var useReflare = async (options = defaultOptions) => {
  const pipeline = usePipeline(useFirewall, useLoadBalancing, useHeaders, useCORS, useUpstream);
  const routeList = [];
  if (options.provider === "static") {
    for (const route of options.routeList) {
      routeList.push(route);
    }
  }
  if (options.provider === "kv") {
    const database = new WorkersKV(options.namespace);
    const routeListKV = await database.get("route-list") || [];
    for (const routeKV of routeListKV) {
      routeList.push(routeKV);
    }
  }
  const handle = async (request) => {
    const route = filter(request, routeList);
    if (route === void 0) {
      return new Response("Failed to find a route that matches the path and method of the current request", {
        status: 500
      });
    }
    const context = {
      request: request.clone(),
      route,
      hostname: getHostname(request),
      response: new Response("Unhandled response"),
      upstream: null
    };
    try {
      await pipeline.execute(context);
    } catch (error) {
      if (error instanceof Error) {
        context.response = new Response(error.message, {
          status: 500
        });
      }
    }
    return context.response;
  };
  const unshift = (route) => {
    routeList.unshift(route);
  };
  const push = (route) => {
    routeList.push(route);
  };
  return {
    handle,
    unshift,
    push
  };
};
var src_default = useReflare;

// src/index.ts
var src_default2 = {
  async fetch(request) {
    const reflare = await src_default();
    reflare.push({
      path: "/*",
      upstream: {
        domain: "api.themoviedb.org",
        protocol: "https"
      },
      cors: {
        origin: "*"
      }
    });
    return reflare.handle(request);
  }
};
export {
  src_default2 as default
};
//# sourceMappingURL=index.js.map

兼容emby客户端么

这样无论哪个平台,安卓或者苹果,电视还是手机都可以用了。谢谢

使用极空间docker安装启动后,打不开内网ip:5245

安装说明里好像并没有针对极空间的说明,不好意思,我是新手,可以帮忙看一下吗?
2023-07-17T22:26:23.778901922Z s6-rc: info: service s6rc-oneshot-runner: starting 2023-07-17T22:26:23.797422442Z s6-rc: info: service s6rc-oneshot-runner successfully started 2023-07-17T22:26:23.798181090Z s6-rc: info: service fix-attrs: starting 2023-07-17T22:26:23.817622430Z s6-rc: info: service fix-attrs successfully started 2023-07-17T22:26:23.818035734Z s6-rc: info: service legacy-cont-init: starting 2023-07-17T22:26:23.835008624Z cont-init: info: running /etc/cont-init.d/010-config 2023-07-17T22:26:25.122043041Z cont-init: info: /etc/cont-init.d/010-config: 初始化成功! 2023-07-17T22:26:29.986874312Z cont-init: info: /etc/cont-init.d/010-config: 修改完config.env配置文件后,运行onelist -run server便可启动项目,忘记密码运行onelist -run admin可查看管理员账户! 2023-07-17T22:26:29.987037650Z cont-init: info: /etc/cont-init.d/010-config: 2023/07/18 06:26:29 初始化缓存系统成功! 2023-07-17T22:26:29.987076443Z cont-init: info: /etc/cont-init.d/010-config: 账号:[email protected] 密码:xxxxx 2023-07-17T22:26:29.987964012Z cont-init: info: /etc/cont-init.d/010-config exited 0 2023-07-17T22:26:29.990223913Z s6-rc: info: service legacy-cont-init successfully started 2023-07-17T22:26:29.990653843Z s6-rc: info: service legacy-services: starting 2023-07-17T22:26:30.032887874Z services-up: info: copying legacy longrun onelist (no readiness notification) 2023-07-17T22:26:30.057252988Z s6-rc: info: service legacy-services successfully started 2023-07-17T22:26:30.858273866Z 2023/07/18 06:26:30 初始化缓存系统成功!

无法播放

能刮削,但是播放不了,F12看到的视频文件地址,拷贝出来直接访问能迅雷下载。

增强刮削能力

目前非常多电影都不能刮削出来 即使是很规范的命名 其他刮削软件可以秒刮削 onelist. 确不认,
能否兼容其他软件的命名规范

建议增加软链接功能
把alist挂载到本地后,onelist 在其他目录建立软链接, 不对实际文件重命名 而是用新文件名建立原文件的软链接 然后 在软链接目录储存刮削信息

可以参考nastools

最好把nastools的刮削代码都移植过来

内存占用较高

尤其是扫库的时候,CPU和内存占用相当高。
如果alist资源新增了,onelist是不是还要完整扫一遍?
另外期待新增telegram讨论组。

签名开启时支持播放

大部分用户是在同一台服务器上挂载alist和onelist,用127.0.0.1访问alist更加安全
若Alist返回302,onelist转发也不会消耗流量

功能建议

能不能搞黄啊挂pikpak搞黄简直太方便了还能海报强。

onelist播放视频相对与alist很卡

在Alist的主页里打开的阿里云盘视频可以很顺滑播放,但是在onelist之中的挂载目录中打开同一视频就会很卡,是不是意味着onelist播放的视频受到服务器带宽的限制?

English language interface

Hi. I just found this project some days ago and I love it but I don't speak Chinese. Do you mind adding an English interface? Or at least add an option to scrape TheMovieDb info in English.
Thank you in advance.

最新版仍然无法播放

设备: N1,已安装最新版alist,版本号3.14,已禁止签名所有。onelist正常刮削成功。但仍然无法播放,用的是阿里open的链接,已确定文件格式为H264的MP4,所以真的不知道我错在哪里? 莫非是隐私内容正则表达式那里出错?求解

媒体库添加挂载目录报错

群晖Docker安装
Alist访问路径有端口号
媒体中心挂载目录,按提示输入/阿里云盘/电视剧
invalid character<`looking for beginning of value
image

提个小问题

音轨不能自己选择 我有个remux的资源 播放出来读的是评论音轨

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.