msterzhang / onelist Goto Github PK
View Code? Open in Web Editor NEW一个类似emby的专注于刮削alist聚合网盘形成影视媒体库的程序。
一个类似emby的专注于刮削alist聚合网盘形成影视媒体库的程序。
是否考虑增加 读取用户自己手动刮削的nfo文件
支持把刮下来的海报图片保存在视频所在的文件里吗?
支持rclone配置aliyundrive-webdav的挂载吗?
是在docker宿主机还是播放端
希望能增加本地刮削功能,有些地方戏剧,我已手动刮削好
能刮削,但是播放不了,F12看到的视频文件地址,拷贝出来直接访问能迅雷下载。
不知道为什么不能浏览器网页在线播放(Chrome、Safari、edge浏览器都不行),一直重新连接,但是用app比如inna打开就能播放。
可以做个安卓的apk或者Magisk吗?
可以借鉴nastools代码,用软连接搜刮,这样子就不影响原文件,而且可以自动改名搜刮。nastools改名搜刮部分优化了很多次,已经可以搜刮到大多数不同的命名格式。
思路:在数据库中新建一张表,用来记录,改名文件在alist中的链接与改过的名字的对应关系。
不建议直接在alist改名。
1.aliist现在用的阿里云盘 Open在同一 IP 在120分钟内请求10次,会出现 TooManyRequests
(例如在 保存/编辑 的时候算一次请求,查看文件看视频下载不算),如果在alist改,容易触发阿里云盘的风控
2.这样更灵活,可以用第三方的手动改名软件本地改名。
大佬能考虑下弄个docker版吗
增加一个自定义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
在alist可以播放,用nplayer显示无效媒体类型
大部分用户是在同一台服务器上挂载alist和onelist,用127.0.0.1访问alist更加安全
若Alist返回302,onelist转发也不会消耗流量
是不是没有找到刮削的资源所以不显示呢?能否可以和jellyfin一样让它显示内容截图?不至于显示不出来
在Alist的主页里打开的阿里云盘视频可以很顺滑播放,但是在onelist之中的挂载目录中打开同一视频就会很卡,是不是意味着onelist播放的视频受到服务器带宽的限制?
添加对trakt 的支持,以方便多平台同步观影记录,
播放器的版本是不是老了,有些视频在Alist上可以正常播放,采集后的Onelist播放不了,视频地址正确。
Alist新版阿里云盘Open调用阿里云盘播放器播放了,Onelist里播放不了。
这样无论哪个平台,安卓或者苹果,电视还是手机都可以用了。谢谢
如题所述,如果能提供刮削接口的文档,我想自己写一个小姐姐刮削的插件
官方文档参考:
其他参考
开发的开源项目参考:
设备: N1,已安装最新版alist,版本号3.14,已禁止签名所有。onelist正常刮削成功。但仍然无法播放,用的是阿里open的链接,已确定文件格式为H264的MP4,所以真的不知道我错在哪里? 莫非是隐私内容正则表达式那里出错?求解
有些视频不能刮削,比如自己拍摄的,不能刮削就不能显示。
能不能搞黄啊挂pikpak搞黄简直太方便了还能海报强。
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.
目前api.themoviedb.org被墙,可以更换为api.tmdb.org,这个域名是可以免翻访问的
举个例子:
aliyun
-XX剧名
-Season 1
-XX S01E01.mp4
调用第三方播放器时复制出来的链接为:
potplayer://域名/d/aliyun/XX剧名/Season 1/XX S01E01.mp4
建议解决方案:参考alist官方进行url转义
potplayer://域名/d/aliyun/XX剧名/Season%201/XX%20S01E01.mp4
不论目录输入什么都会提示invalid character '<' looking for beginning of value,请问是什么问题?是因为我的alist也是docker吗?
全都显示get number error要怎么解决
希望更新访客模式,这样其他人就不用输入账号密码登录了
如题
docker部署,提交时提示以下报错信息
context deadline exceeded (Client.Timeout exceeded while awaiting headers)
问题描述:
windows端本地部署 onelist-windows-386.zip,打开报错[GIN-debug] Listening and serving HTTP on :0
显示监听端口为0,尝试打开 5245,5246,8080都失败。
可能为随机端口或端口冲突
manifest for docker.io/msterzhang/onelist:latest not found
是改仓库了吗
安装说明里好像并没有针对极空间的说明,不好意思,我是新手,可以帮忙看一下吗?
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 初始化缓存系统成功!
我的资源已刮削过,感觉可以添加个识别导入的选项
音轨不能自己选择 我有个remux的资源 播放出来读的是评论音轨
监控目录,每晚自动扫描的时间能否自定义或者改为每周?多谢多谢!
尤其是扫库的时候,CPU和内存占用相当高。
如果alist资源新增了,onelist是不是还要完整扫一遍?
另外期待新增telegram讨论组。
目前非常多电影都不能刮削出来 即使是很规范的命名 其他刮削软件可以秒刮削 onelist. 确不认,
能否兼容其他软件的命名规范
建议增加软链接功能
把alist挂载到本地后,onelist 在其他目录建立软链接, 不对实际文件重命名 而是用新文件名建立原文件的软链接 然后 在软链接目录储存刮削信息
可以参考nastools
最好把nastools的刮削代码都移植过来
目前安装 arm64版本 执行./onelist -run config 会报 bash: ./onelist: cannot execute: required file not found
是否可以用个壳子展示onelist网页?这样在电视上看就很方便了
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.