Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

自动下载和搜索有较大的延迟 #1

Closed
zzz6839 opened this issue Aug 14, 2021 · 11 comments
Closed

自动下载和搜索有较大的延迟 #1

zzz6839 opened this issue Aug 14, 2021 · 11 comments

Comments

@zzz6839
Copy link

zzz6839 commented Aug 14, 2021

通常文件在dmhy发布的两个小时后才能通过dandanplay搜索到资源。以我们的重制人生为例,资源发布在dmhy时间为晚8:30左右,自动下载和在弹弹play里的搜索相关文件会延迟到凌晨1点左右统一发布,不知道是不是BUG。

@LussacZheng
Copy link
Owner

你使用的是 golang 还是 cf-worker 版本?

目前 golang 版本和 cf-worker 版本都没有缓存机制,二者都是在监听到 弹弹play 客户端发起的 API 请求后,实时访问 dmhy 的相关搜索页面,从 HTML 中解析提取信息并返回。

按道理 "API 返回的信息" 应该和 "在浏览器中的搜索结果" 是一致的。


按照你的描述进行测试,当前 dmhy 主页的第一项为 "恶魔恋歌" ,为 2021-08-14_17:00 发布,

主页

而在 2021-08-14_17:28 时进行的 搜索结果 中并未出现。

搜索页

说明此时 dmhy 可能尚未将此条资源添加到索引中,即 "延迟" 可能是由 dmhy 本身引起的。

@zzz6839 zzz6839 closed this as completed Aug 14, 2021
@zzz6839
Copy link
Author

zzz6839 commented Aug 14, 2021

了解了,3Q

@LussacZheng
Copy link
Owner

我用 cf-worker 版本简单实现了一个 "搜索指令/选项" 的功能:
在原来搜索的关键词后添加 $realtime ,即可要求 API 额外访问一次 首页 ,从首页中根据关键词,找到由于 "延迟" 而未出现在搜索页中的资源,再一并返回给客户端。

如果你感兴趣的话,可以帮我测试一下。

具体效果如下:

screen-recode

worker.js 代码如下(代码块的右上角有复制按钮):

// https://github.com/LussacZheng/dandanplay-resource-service
// version: 0.0.4-alpha
// build: 2021-08-15 01:31 GMT+8
// wrangler: 1.19.0

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([function(e){e.exports=JSON.parse('{"b":"0.0.4-alpha","a":"https://github.com/LussacZheng/dandanplay-resource-service"}')},function(e,t,n){"use strict";n.r(t);class r{constructor(e){const{keyword:t,options:n}=function(e){let t={};return{keyword:e.replace(/ ?\$(\w+)(:(\d+))?/gi,(e,n,r,o)=>(t[n]=parseInt(o)||1,"")),options:t}}(e);this.keyword=t,this.realtime=n.realtime||0}}function o(e,t){return e.replace(/\$\{(\w+)\}/gi,(e,n)=>t[n])}function a(e){const t=new Date(e).toLocaleString("default",{formatMatcher:"best fit",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"});return new Date(t+" UTC").toISOString().substr(0,19).replace("T"," ")}function s(e,t){if(0===t.length)return e[1];let n={};return t.length>e.length-1&&t.splice(e.length-1),t.forEach((t,r)=>{n[t]=e[r+1]}),n}var i=function(e,t,n,r="first"){switch(r){case"all":return function(e,t,n){const r=e.matchAll(t),o=Array.from(r,e=>s(e,n));return 0===o.length?null:o}(e,t,n);case"last":return function(e,t,n){const r=[...e.matchAll(t)],o=r[r.length-1];return 0===r.length?null:s(o,n)}(e,t,n);default:return function(e,t,n){let r=t.exec(e);return t.lastIndex=0,null===r?null:s(r,n)}(e,t,n)}};const u={headers:{"content-type":"application/json;charset=utf-8"}},l={headers:{"content-type":"text/html;charset=utf-8"}},c={headers:{accept:"text/html;charset=utf-8","user-agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"}};async function d(e,t=c){let n;try{n=await fetch(decodeURI(e),t)}catch(e){console.error(e)}return await async function(e){const{headers:t}=e,n=t.get("content-type");return n.includes("application/json")?await e.json():(n.includes("application/text")||n.includes("text/html"),await e.text())}(n)}const p="https://share.dmhy.org",h={type_and_subgroup_url:p+"/topics/advanced-search?team_id=0&sort_id=0&orderby=",list_url:p+"/topics/list/page/1?keyword=${keyword}&sort_id=${type}&team_id=${subgroup}&order=date-desc",index_url:p+"/topics/list/page/${realtime}"},g="未能成功解析标题",f=-2,y="未能成功解析类别",m=-1,w="未知字幕组",b="magnet_not_found_未能成功解析磁力链接或磁力链接不存在",_="未能成功解析资源发布页面",I="未能成功解析资源大小",v="1970-01-01 08:00:00",R=/<option value="(\d+)">(.+?)<\/option>/gim,S=/<option value="(\d+)" style="color: [\w#]+">(.+?)<\/option>/gim,x={HasMore:/下一頁/g,Resources:/<tr class="">(.*?)<\/tr>/gis,TypeId:/href="\/topics\/list\/sort_id\/(\d+)"/gim,TypeName:/<font color=[\w#]+>(.+)<\/font>/gim,SubgroupId:/href="\/topics\/list\/team_id\/(\d+)"/gim,SubgroupName:/\s+(.*)<\/a><\/span>/gim,Magnet:/href="(magnet:\?xt=urn:btih:.+?)"/gim,PageUrl:/href="(.+?)"\s*target="_blank"/gim,FileSize:/<td.*>([\w\.]+B)<\/td>/gim,PublishDate:/<span style="display: none;">([\d\/ :]+)<\/span>/gim,Title:/target="_blank" ?>(.+?)<\/a>/gis,TitleReplacer:/<span class="keyword">(.*?)<\/span>/gi};async function T(e){const t=new URL(encodeURI(e.url)).searchParams,n=t.get("type")||0,a=t.get("subgroup")||0,{keyword:s,realtime:u}=new r(decodeURIComponent(t.get("keyword"))),l=encodeURI(o(h.list_url,{keyword:s,type:n<0?0:n,subgroup:a<0?0:a}));let c=await d(l),p=function(e){let t={HasMore:null!==i(e,x.HasMore,[]),Resources:[]};const n=i(e,x.Resources,[],"all");return null===n||n.forEach(e=>{t.Resources.push(O(e))}),t}(c);if(u){const e=encodeURI(o(h.index_url,{realtime:u}));c=await d(e);const t=U(c,s,p.Resources);p.Resources=t.concat(p.Resources)}return p}function N(e){const t=e.replace(/&amp;/gi,"&");let n=i(t,R,["Id","Name"],"all");return null===n?[]:(n.forEach(e=>e.Id=parseInt(e.Id)),n.shift(),n)}function P(e){let t=i(e,S,["Id","Name"],"all");return null===t?[]:(t.forEach(e=>e.Id=parseInt(e.Id)),t.unshift({Id:0,Name:"全部"}),t)}function U(e,t,n){let r=[];const o=i(e,x.Resources,[],"all");return null===o?result:(o.forEach(e=>{let o=O(e);const a=t.split(" ").every(e=>o.Title.includes(e)),s=n.some(e=>o.PageUrl===e.PageUrl);a&&!s&&r.push(o)}),r)}function O(e){const t=i(e,x.Title,[]),n=i(e,x.TypeId,[]),r=i(e,x.TypeName,[]),o=i(e,x.SubgroupId,[]),s=i(e,x.SubgroupName,[]),u=i(e,x.Magnet,[]),l=i(e,x.PageUrl,[]),c=i(e,x.FileSize,[]),d=i(e,x.PublishDate,[]);return{Title:null===t?g:t.trim().replace(x.TitleReplacer,"$1"),TypeId:parseInt(n)||f,TypeName:r||y,SubgroupId:parseInt(o)||m,SubgroupName:s||w,Magnet:u||b,PageUrl:null===l?_:p+l,FileSize:c||I,PublishDate:null===d?v:a(d)}}var k=n(0);const M=`\n<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n  <meta charset="UTF-8" />\n  <meta name="viewport" content="width=device-width,initial-scale=1" />\n  <title>弹弹play资源搜索节点API - v${k.b}</title>\n</head>\n<body>\n  <h1>使用说明</h1>\n  <h2>GitHub - <a href="${k.a}">LussacZheng/dandanplay-resource-service</a></h2>\n</body>\n</html>\n`;const j=e=>t=>t.method.toLowerCase()===e.toLowerCase(),L=j("connect"),$=j("delete"),E=j("get"),C=j("head"),A=j("options"),D=j("patch"),z=j("post"),F=j("put"),H=j("trace"),J=e=>t=>{const n=new URL(encodeURI(t.url)).pathname;return(n.match(e)||[])[0]===n};var W=class{constructor(){this.routes=[]}handle(e,t){return this.routes.push({conditions:e,handler:t}),this}connect(e,t){return this.handle([L,J(e)],t)}delete(e,t){return this.handle([$,J(e)],t)}get(e,t){return this.handle([E,J(e)],t)}head(e,t){return this.handle([C,J(e)],t)}options(e,t){return this.handle([A,J(e)],t)}patch(e,t){return this.handle([D,J(e)],t)}post(e,t){return this.handle([z,J(e)],t)}put(e,t){return this.handle([F,J(e)],t)}trace(e,t){return this.handle([H,J(e)],t)}all(e){return this.handle([],e)}route(e){const t=this.resolve(e);return t?t.handler(e):new Response("resource not found",{status:404,statusText:"not found",headers:{"content-type":"text/plain"}})}resolve(e){return this.routes.find(t=>!(t.conditions&&(!Array.isArray(t)||t.conditions.length))||("function"==typeof t.conditions?t.conditions(e):t.conditions.every(t=>t(e))))}};async function Z(e){const t=new W;t.get("/subgroup",async()=>{const e=await async function(){return{Subgroups:N(await d(h.type_and_subgroup_url))}}();return new Response(JSON.stringify(e),u)}),t.get("/type",async()=>{const e=await async function(){return{Types:P(await d(h.type_and_subgroup_url))}}();return new Response(JSON.stringify(e),u)}),t.get("/list",async e=>{const t=await T(e);return new Response(JSON.stringify(t),u)}),t.get("/",async()=>new Response(await async function(){let e;try{e=await d("https://cdn.jsdelivr.net/gh/LussacZheng/dandanplay-resource-service@dist/web/index.html"),e=o(e,{VERSION:k.b})}catch(t){e=M}return e}(),l));return await t.route(e)}addEventListener("fetch",e=>e.respondWith(Z(e.request)))}]);

目前的缺点如下:

  • 使用 $realtime 时,只允许空格这一种符号。不能与带 & | ! ( ) 的高级搜索同时使用。
  • $realtime 不支持 "延迟" 资源的简繁体转换。即关键词为简中时,同名的繁中 "延迟" 资源不会出现在结果中。

此外,$realtime 还支持指定页码。如 $realtime:2 表示从 第二页 中寻找 "延迟" 资源,而非 首页
当然这个可能没什么用,这么设计主要时为了以后可以添加更多的 "搜索指令/选项" ,如:

  • $page:n : 获取搜索结果的第 n 页,而不是总是返回第一页
  • $limit:n : 限制搜索结果的数量上限为 n ,而不是总为 80
  • ......

@zzz6839
Copy link
Author

zzz6839 commented Aug 15, 2021

我用 cf-worker 版本简单实现了一个 "搜索指令/选项" 的功能:
在原来搜索的关键词后添加 $realtime ,即可要求 API 额外访问一次 首页 ,从首页中根据关键词,找到由于 "延迟" 而未出现在搜索页中的资源,再一并返回给客户端。

如果你感兴趣的话,可以帮我测试一下。

具体效果如下:

screen-recode

worker.js 代码如下(代码块的右上角有复制按钮):

// https://github.com/LussacZheng/dandanplay-resource-service
// version: 0.0.4-alpha
// build: 2021-08-15 01:31 GMT+8
// wrangler: 1.19.0

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([function(e){e.exports=JSON.parse('{"b":"0.0.4-alpha","a":"https://github.com/LussacZheng/dandanplay-resource-service"}')},function(e,t,n){"use strict";n.r(t);class r{constructor(e){const{keyword:t,options:n}=function(e){let t={};return{keyword:e.replace(/ ?\$(\w+)(:(\d+))?/gi,(e,n,r,o)=>(t[n]=parseInt(o)||1,"")),options:t}}(e);this.keyword=t,this.realtime=n.realtime||0}}function o(e,t){return e.replace(/\$\{(\w+)\}/gi,(e,n)=>t[n])}function a(e){const t=new Date(e).toLocaleString("default",{formatMatcher:"best fit",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"});return new Date(t+" UTC").toISOString().substr(0,19).replace("T"," ")}function s(e,t){if(0===t.length)return e[1];let n={};return t.length>e.length-1&&t.splice(e.length-1),t.forEach((t,r)=>{n[t]=e[r+1]}),n}var i=function(e,t,n,r="first"){switch(r){case"all":return function(e,t,n){const r=e.matchAll(t),o=Array.from(r,e=>s(e,n));return 0===o.length?null:o}(e,t,n);case"last":return function(e,t,n){const r=[...e.matchAll(t)],o=r[r.length-1];return 0===r.length?null:s(o,n)}(e,t,n);default:return function(e,t,n){let r=t.exec(e);return t.lastIndex=0,null===r?null:s(r,n)}(e,t,n)}};const u={headers:{"content-type":"application/json;charset=utf-8"}},l={headers:{"content-type":"text/html;charset=utf-8"}},c={headers:{accept:"text/html;charset=utf-8","user-agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"}};async function d(e,t=c){let n;try{n=await fetch(decodeURI(e),t)}catch(e){console.error(e)}return await async function(e){const{headers:t}=e,n=t.get("content-type");return n.includes("application/json")?await e.json():(n.includes("application/text")||n.includes("text/html"),await e.text())}(n)}const p="https://share.dmhy.org",h={type_and_subgroup_url:p+"/topics/advanced-search?team_id=0&sort_id=0&orderby=",list_url:p+"/topics/list/page/1?keyword=${keyword}&sort_id=${type}&team_id=${subgroup}&order=date-desc",index_url:p+"/topics/list/page/${realtime}"},g="未能成功解析标题",f=-2,y="未能成功解析类别",m=-1,w="未知字幕组",b="magnet_not_found_未能成功解析磁力链接或磁力链接不存在",_="未能成功解析资源发布页面",I="未能成功解析资源大小",v="1970-01-01 08:00:00",R=/<option value="(\d+)">(.+?)<\/option>/gim,S=/<option value="(\d+)" style="color: [\w#]+">(.+?)<\/option>/gim,x={HasMore:/下一頁/g,Resources:/<tr class="">(.*?)<\/tr>/gis,TypeId:/href="\/topics\/list\/sort_id\/(\d+)"/gim,TypeName:/<font color=[\w#]+>(.+)<\/font>/gim,SubgroupId:/href="\/topics\/list\/team_id\/(\d+)"/gim,SubgroupName:/\s+(.*)<\/a><\/span>/gim,Magnet:/href="(magnet:\?xt=urn:btih:.+?)"/gim,PageUrl:/href="(.+?)"\s*target="_blank"/gim,FileSize:/<td.*>([\w\.]+B)<\/td>/gim,PublishDate:/<span style="display: none;">([\d\/ :]+)<\/span>/gim,Title:/target="_blank" ?>(.+?)<\/a>/gis,TitleReplacer:/<span class="keyword">(.*?)<\/span>/gi};async function T(e){const t=new URL(encodeURI(e.url)).searchParams,n=t.get("type")||0,a=t.get("subgroup")||0,{keyword:s,realtime:u}=new r(decodeURIComponent(t.get("keyword"))),l=encodeURI(o(h.list_url,{keyword:s,type:n<0?0:n,subgroup:a<0?0:a}));let c=await d(l),p=function(e){let t={HasMore:null!==i(e,x.HasMore,[]),Resources:[]};const n=i(e,x.Resources,[],"all");return null===n||n.forEach(e=>{t.Resources.push(O(e))}),t}(c);if(u){const e=encodeURI(o(h.index_url,{realtime:u}));c=await d(e);const t=U(c,s,p.Resources);p.Resources=t.concat(p.Resources)}return p}function N(e){const t=e.replace(/&amp;/gi,"&");let n=i(t,R,["Id","Name"],"all");return null===n?[]:(n.forEach(e=>e.Id=parseInt(e.Id)),n.shift(),n)}function P(e){let t=i(e,S,["Id","Name"],"all");return null===t?[]:(t.forEach(e=>e.Id=parseInt(e.Id)),t.unshift({Id:0,Name:"全部"}),t)}function U(e,t,n){let r=[];const o=i(e,x.Resources,[],"all");return null===o?result:(o.forEach(e=>{let o=O(e);const a=t.split(" ").every(e=>o.Title.includes(e)),s=n.some(e=>o.PageUrl===e.PageUrl);a&&!s&&r.push(o)}),r)}function O(e){const t=i(e,x.Title,[]),n=i(e,x.TypeId,[]),r=i(e,x.TypeName,[]),o=i(e,x.SubgroupId,[]),s=i(e,x.SubgroupName,[]),u=i(e,x.Magnet,[]),l=i(e,x.PageUrl,[]),c=i(e,x.FileSize,[]),d=i(e,x.PublishDate,[]);return{Title:null===t?g:t.trim().replace(x.TitleReplacer,"$1"),TypeId:parseInt(n)||f,TypeName:r||y,SubgroupId:parseInt(o)||m,SubgroupName:s||w,Magnet:u||b,PageUrl:null===l?_:p+l,FileSize:c||I,PublishDate:null===d?v:a(d)}}var k=n(0);const M=`\n<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n  <meta charset="UTF-8" />\n  <meta name="viewport" content="width=device-width,initial-scale=1" />\n  <title>弹弹play资源搜索节点API - v${k.b}</title>\n</head>\n<body>\n  <h1>使用说明</h1>\n  <h2>GitHub - <a href="${k.a}">LussacZheng/dandanplay-resource-service</a></h2>\n</body>\n</html>\n`;const j=e=>t=>t.method.toLowerCase()===e.toLowerCase(),L=j("connect"),$=j("delete"),E=j("get"),C=j("head"),A=j("options"),D=j("patch"),z=j("post"),F=j("put"),H=j("trace"),J=e=>t=>{const n=new URL(encodeURI(t.url)).pathname;return(n.match(e)||[])[0]===n};var W=class{constructor(){this.routes=[]}handle(e,t){return this.routes.push({conditions:e,handler:t}),this}connect(e,t){return this.handle([L,J(e)],t)}delete(e,t){return this.handle([$,J(e)],t)}get(e,t){return this.handle([E,J(e)],t)}head(e,t){return this.handle([C,J(e)],t)}options(e,t){return this.handle([A,J(e)],t)}patch(e,t){return this.handle([D,J(e)],t)}post(e,t){return this.handle([z,J(e)],t)}put(e,t){return this.handle([F,J(e)],t)}trace(e,t){return this.handle([H,J(e)],t)}all(e){return this.handle([],e)}route(e){const t=this.resolve(e);return t?t.handler(e):new Response("resource not found",{status:404,statusText:"not found",headers:{"content-type":"text/plain"}})}resolve(e){return this.routes.find(t=>!(t.conditions&&(!Array.isArray(t)||t.conditions.length))||("function"==typeof t.conditions?t.conditions(e):t.conditions.every(t=>t(e))))}};async function Z(e){const t=new W;t.get("/subgroup",async()=>{const e=await async function(){return{Subgroups:N(await d(h.type_and_subgroup_url))}}();return new Response(JSON.stringify(e),u)}),t.get("/type",async()=>{const e=await async function(){return{Types:P(await d(h.type_and_subgroup_url))}}();return new Response(JSON.stringify(e),u)}),t.get("/list",async e=>{const t=await T(e);return new Response(JSON.stringify(t),u)}),t.get("/",async()=>new Response(await async function(){let e;try{e=await d("https://cdn.jsdelivr.net/gh/LussacZheng/dandanplay-resource-service@dist/web/index.html"),e=o(e,{VERSION:k.b})}catch(t){e=M}return e}(),l));return await t.route(e)}addEventListener("fetch",e=>e.respondWith(Z(e.request)))}]);

目前的缺点如下:

  • 使用 $realtime 时,只允许空格这一种符号。不能与带 & | ! ( ) 的高级搜索同时使用。
  • $realtime 不支持 "延迟" 资源的简繁体转换。即关键词为简中时,同名的繁中 "延迟" 资源不会出现在结果中。

此外,$realtime 还支持指定页码。如 $realtime:2 表示从 第二页 中寻找 "延迟" 资源,而非 首页
当然这个可能没什么用,这么设计主要时为了以后可以添加更多的 "搜索指令/选项" ,如:

  • $page:n : 获取搜索结果的第 n 页,而不是总是返回第一页
  • $limit:n : 限制搜索结果的数量上限为 n ,而不是总为 80
  • ......

感谢你的回复,我测试过新的脚本可以实现实时搜索的功能,并且可以配合自动下载关键词功能实现资源的实时自动下载,并且如你所反映的,无法实现简繁体的转换,不过对于我来说已经很实用了,谢谢(我用的是cf-worker)。
1

@zzz6839
Copy link
Author

zzz6839 commented Aug 15, 2021

我用 cf-worker 版本简单实现了一个 "搜索指令/选项" 的功能:
在原来搜索的关键词后添加 $realtime ,即可要求 API 额外访问一次 首页 ,从首页中根据关键词,找到由于 "延迟" 而未出现在搜索页中的资源,再一并返回给客户端。
如果你感兴趣的话,可以帮我测试一下。
具体效果如下:
screen-recode
worker.js 代码如下(代码块的右上角有复制按钮):

// https://github.com/LussacZheng/dandanplay-resource-service
// version: 0.0.4-alpha
// build: 2021-08-15 01:31 GMT+8
// wrangler: 1.19.0

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([function(e){e.exports=JSON.parse('{"b":"0.0.4-alpha","a":"https://github.com/LussacZheng/dandanplay-resource-service"}')},function(e,t,n){"use strict";n.r(t);class r{constructor(e){const{keyword:t,options:n}=function(e){let t={};return{keyword:e.replace(/ ?\$(\w+)(:(\d+))?/gi,(e,n,r,o)=>(t[n]=parseInt(o)||1,"")),options:t}}(e);this.keyword=t,this.realtime=n.realtime||0}}function o(e,t){return e.replace(/\$\{(\w+)\}/gi,(e,n)=>t[n])}function a(e){const t=new Date(e).toLocaleString("default",{formatMatcher:"best fit",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"});return new Date(t+" UTC").toISOString().substr(0,19).replace("T"," ")}function s(e,t){if(0===t.length)return e[1];let n={};return t.length>e.length-1&&t.splice(e.length-1),t.forEach((t,r)=>{n[t]=e[r+1]}),n}var i=function(e,t,n,r="first"){switch(r){case"all":return function(e,t,n){const r=e.matchAll(t),o=Array.from(r,e=>s(e,n));return 0===o.length?null:o}(e,t,n);case"last":return function(e,t,n){const r=[...e.matchAll(t)],o=r[r.length-1];return 0===r.length?null:s(o,n)}(e,t,n);default:return function(e,t,n){let r=t.exec(e);return t.lastIndex=0,null===r?null:s(r,n)}(e,t,n)}};const u={headers:{"content-type":"application/json;charset=utf-8"}},l={headers:{"content-type":"text/html;charset=utf-8"}},c={headers:{accept:"text/html;charset=utf-8","user-agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"}};async function d(e,t=c){let n;try{n=await fetch(decodeURI(e),t)}catch(e){console.error(e)}return await async function(e){const{headers:t}=e,n=t.get("content-type");return n.includes("application/json")?await e.json():(n.includes("application/text")||n.includes("text/html"),await e.text())}(n)}const p="https://share.dmhy.org",h={type_and_subgroup_url:p+"/topics/advanced-search?team_id=0&sort_id=0&orderby=",list_url:p+"/topics/list/page/1?keyword=${keyword}&sort_id=${type}&team_id=${subgroup}&order=date-desc",index_url:p+"/topics/list/page/${realtime}"},g="未能成功解析标题",f=-2,y="未能成功解析类别",m=-1,w="未知字幕组",b="magnet_not_found_未能成功解析磁力链接或磁力链接不存在",_="未能成功解析资源发布页面",I="未能成功解析资源大小",v="1970-01-01 08:00:00",R=/<option value="(\d+)">(.+?)<\/option>/gim,S=/<option value="(\d+)" style="color: [\w#]+">(.+?)<\/option>/gim,x={HasMore:/下一頁/g,Resources:/<tr class="">(.*?)<\/tr>/gis,TypeId:/href="\/topics\/list\/sort_id\/(\d+)"/gim,TypeName:/<font color=[\w#]+>(.+)<\/font>/gim,SubgroupId:/href="\/topics\/list\/team_id\/(\d+)"/gim,SubgroupName:/\s+(.*)<\/a><\/span>/gim,Magnet:/href="(magnet:\?xt=urn:btih:.+?)"/gim,PageUrl:/href="(.+?)"\s*target="_blank"/gim,FileSize:/<td.*>([\w\.]+B)<\/td>/gim,PublishDate:/<span style="display: none;">([\d\/ :]+)<\/span>/gim,Title:/target="_blank" ?>(.+?)<\/a>/gis,TitleReplacer:/<span class="keyword">(.*?)<\/span>/gi};async function T(e){const t=new URL(encodeURI(e.url)).searchParams,n=t.get("type")||0,a=t.get("subgroup")||0,{keyword:s,realtime:u}=new r(decodeURIComponent(t.get("keyword"))),l=encodeURI(o(h.list_url,{keyword:s,type:n<0?0:n,subgroup:a<0?0:a}));let c=await d(l),p=function(e){let t={HasMore:null!==i(e,x.HasMore,[]),Resources:[]};const n=i(e,x.Resources,[],"all");return null===n||n.forEach(e=>{t.Resources.push(O(e))}),t}(c);if(u){const e=encodeURI(o(h.index_url,{realtime:u}));c=await d(e);const t=U(c,s,p.Resources);p.Resources=t.concat(p.Resources)}return p}function N(e){const t=e.replace(/&amp;/gi,"&");let n=i(t,R,["Id","Name"],"all");return null===n?[]:(n.forEach(e=>e.Id=parseInt(e.Id)),n.shift(),n)}function P(e){let t=i(e,S,["Id","Name"],"all");return null===t?[]:(t.forEach(e=>e.Id=parseInt(e.Id)),t.unshift({Id:0,Name:"全部"}),t)}function U(e,t,n){let r=[];const o=i(e,x.Resources,[],"all");return null===o?result:(o.forEach(e=>{let o=O(e);const a=t.split(" ").every(e=>o.Title.includes(e)),s=n.some(e=>o.PageUrl===e.PageUrl);a&&!s&&r.push(o)}),r)}function O(e){const t=i(e,x.Title,[]),n=i(e,x.TypeId,[]),r=i(e,x.TypeName,[]),o=i(e,x.SubgroupId,[]),s=i(e,x.SubgroupName,[]),u=i(e,x.Magnet,[]),l=i(e,x.PageUrl,[]),c=i(e,x.FileSize,[]),d=i(e,x.PublishDate,[]);return{Title:null===t?g:t.trim().replace(x.TitleReplacer,"$1"),TypeId:parseInt(n)||f,TypeName:r||y,SubgroupId:parseInt(o)||m,SubgroupName:s||w,Magnet:u||b,PageUrl:null===l?_:p+l,FileSize:c||I,PublishDate:null===d?v:a(d)}}var k=n(0);const M=`\n<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n  <meta charset="UTF-8" />\n  <meta name="viewport" content="width=device-width,initial-scale=1" />\n  <title>弹弹play资源搜索节点API - v${k.b}</title>\n</head>\n<body>\n  <h1>使用说明</h1>\n  <h2>GitHub - <a href="${k.a}">LussacZheng/dandanplay-resource-service</a></h2>\n</body>\n</html>\n`;const j=e=>t=>t.method.toLowerCase()===e.toLowerCase(),L=j("connect"),$=j("delete"),E=j("get"),C=j("head"),A=j("options"),D=j("patch"),z=j("post"),F=j("put"),H=j("trace"),J=e=>t=>{const n=new URL(encodeURI(t.url)).pathname;return(n.match(e)||[])[0]===n};var W=class{constructor(){this.routes=[]}handle(e,t){return this.routes.push({conditions:e,handler:t}),this}connect(e,t){return this.handle([L,J(e)],t)}delete(e,t){return this.handle([$,J(e)],t)}get(e,t){return this.handle([E,J(e)],t)}head(e,t){return this.handle([C,J(e)],t)}options(e,t){return this.handle([A,J(e)],t)}patch(e,t){return this.handle([D,J(e)],t)}post(e,t){return this.handle([z,J(e)],t)}put(e,t){return this.handle([F,J(e)],t)}trace(e,t){return this.handle([H,J(e)],t)}all(e){return this.handle([],e)}route(e){const t=this.resolve(e);return t?t.handler(e):new Response("resource not found",{status:404,statusText:"not found",headers:{"content-type":"text/plain"}})}resolve(e){return this.routes.find(t=>!(t.conditions&&(!Array.isArray(t)||t.conditions.length))||("function"==typeof t.conditions?t.conditions(e):t.conditions.every(t=>t(e))))}};async function Z(e){const t=new W;t.get("/subgroup",async()=>{const e=await async function(){return{Subgroups:N(await d(h.type_and_subgroup_url))}}();return new Response(JSON.stringify(e),u)}),t.get("/type",async()=>{const e=await async function(){return{Types:P(await d(h.type_and_subgroup_url))}}();return new Response(JSON.stringify(e),u)}),t.get("/list",async e=>{const t=await T(e);return new Response(JSON.stringify(t),u)}),t.get("/",async()=>new Response(await async function(){let e;try{e=await d("https://cdn.jsdelivr.net/gh/LussacZheng/dandanplay-resource-service@dist/web/index.html"),e=o(e,{VERSION:k.b})}catch(t){e=M}return e}(),l));return await t.route(e)}addEventListener("fetch",e=>e.respondWith(Z(e.request)))}]);

目前的缺点如下:

  • 使用 $realtime 时,只允许空格这一种符号。不能与带 & | ! ( ) 的高级搜索同时使用。
  • $realtime 不支持 "延迟" 资源的简繁体转换。即关键词为简中时,同名的繁中 "延迟" 资源不会出现在结果中。

此外,$realtime 还支持指定页码。如 $realtime:2 表示从 第二页 中寻找 "延迟" 资源,而非 首页
当然这个可能没什么用,这么设计主要时为了以后可以添加更多的 "搜索指令/选项" ,如:

  • $page:n : 获取搜索结果的第 n 页,而不是总是返回第一页
  • $limit:n : 限制搜索结果的数量上限为 n ,而不是总为 80
  • ......

感谢你的回复,我测试过新的脚本可以实现实时搜索的功能,并且可以配合自动下载关键词功能实现资源的实时自动下载,并且如你所反映的,无法实现简繁体的转换,不过对于我来说已经很实用了,谢谢(我用的是cf-worker)。
1

@zzz6839 zzz6839 reopened this Aug 15, 2021
@zzz6839
Copy link
Author

zzz6839 commented Aug 15, 2021

我发现这个新的脚本不支持多个关键词搜索,以侦探已死为例,如果搜索关键词“侦探已死”+“$realtime”可以得到实时结果,但是如果进一步增加关键词比如“侦探已死”+“$realtime”+“global”就无法得到实时数据,希望可以修复一下,或者在脚本里集成realtime search的功能

@LussacZheng
Copy link
Owner

LussacZheng commented Aug 15, 2021

其实是支持的,只是我忘记了在使用 $realtime 时,应该将关键词 toLowerCase() 即全部转换为小写字母后,再进行比较。
对于之前的脚本,你使用 "侦探已死" + "$realtime" + "Global" 应该是可以得到实时数据的。

修复后的 worker.js 如下:

// https://github.com/LussacZheng/dandanplay-resource-service
// version: 0.0.4-alpha.1
// build: 2021-08-15 22:41 GMT+8
// wrangler: 1.19.0

!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([function(e){e.exports=JSON.parse('{"b":"0.0.4-alpha.1","a":"https://github.com/LussacZheng/dandanplay-resource-service"}')},function(e,t,n){"use strict";n.r(t);class r{constructor(e){const{keyword:t,options:n}=function(e){let t={};return{keyword:e.replace(/ ?\$(\w+)(:(\d+))?/gi,(e,n,r,o)=>(t[n]=parseInt(o)||1,"")),options:t}}(e);this.keyword=t,this.realtime=n.realtime||0}}function o(e,t){return e.replace(/\$\{(\w+)\}/gi,(e,n)=>t[n])}function a(e){const t=new Date(e).toLocaleString("default",{formatMatcher:"best fit",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"});return new Date(t+" UTC").toISOString().substr(0,19).replace("T"," ")}function s(e,t){if(0===t.length)return e[1];let n={};return t.length>e.length-1&&t.splice(e.length-1),t.forEach((t,r)=>{n[t]=e[r+1]}),n}var i=function(e,t,n,r="first"){switch(r){case"all":return function(e,t,n){const r=e.matchAll(t),o=Array.from(r,e=>s(e,n));return 0===o.length?null:o}(e,t,n);case"last":return function(e,t,n){const r=[...e.matchAll(t)],o=r[r.length-1];return 0===r.length?null:s(o,n)}(e,t,n);default:return function(e,t,n){let r=t.exec(e);return t.lastIndex=0,null===r?null:s(r,n)}(e,t,n)}};const u={headers:{"content-type":"application/json;charset=utf-8"}},l={headers:{"content-type":"text/html;charset=utf-8"}},c={headers:{accept:"text/html;charset=utf-8","user-agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"}};async function d(e,t=c){let n;try{n=await fetch(decodeURI(e),t)}catch(e){console.error(e)}return await async function(e){const{headers:t}=e,n=t.get("content-type");return n.includes("application/json")?await e.json():(n.includes("application/text")||n.includes("text/html"),await e.text())}(n)}const p="https://share.dmhy.org",h={type_and_subgroup_url:p+"/topics/advanced-search?team_id=0&sort_id=0&orderby=",list_url:p+"/topics/list/page/1?keyword=${keyword}&sort_id=${type}&team_id=${subgroup}&order=date-desc",index_url:p+"/topics/list/page/${realtime}"},g="未能成功解析标题",f=-2,y="未能成功解析类别",m=-1,w="未知字幕组",b="magnet_not_found_未能成功解析磁力链接或磁力链接不存在",_="未能成功解析资源发布页面",I="未能成功解析资源大小",v="1970-01-01 08:00:00",R=/<option value="(\d+)">(.+?)<\/option>/gim,S=/<option value="(\d+)" style="color: [\w#]+">(.+?)<\/option>/gim,x={HasMore:/下一頁/g,Resources:/<tr class="">(.*?)<\/tr>/gis,TypeId:/href="\/topics\/list\/sort_id\/(\d+)"/gim,TypeName:/<font color=[\w#]+>(.+)<\/font>/gim,SubgroupId:/href="\/topics\/list\/team_id\/(\d+)"/gim,SubgroupName:/\s+(.*)<\/a><\/span>/gim,Magnet:/href="(magnet:\?xt=urn:btih:.+?)"/gim,PageUrl:/href="(.+?)"\s*target="_blank"/gim,FileSize:/<td.*>([\w\.]+B)<\/td>/gim,PublishDate:/<span style="display: none;">([\d\/ :]+)<\/span>/gim,Title:/target="_blank" ?>(.+?)<\/a>/gis,TitleReplacer:/<span class="keyword">(.*?)<\/span>/gi};async function T(e){const t=new URL(encodeURI(e.url)).searchParams,n=t.get("type")||0,a=t.get("subgroup")||0,{keyword:s,realtime:u}=new r(decodeURIComponent(t.get("keyword"))),l=encodeURI(o(h.list_url,{keyword:s,type:n<0?0:n,subgroup:a<0?0:a}));let c=await d(l),p=function(e){let t={HasMore:null!==i(e,x.HasMore,[]),Resources:[]};const n=i(e,x.Resources,[],"all");return null===n||n.forEach(e=>{t.Resources.push(O(e))}),t}(c);if(u){const e=encodeURI(o(h.index_url,{realtime:u}));c=await d(e);const t=U(c,s,p.Resources);p.Resources=t.concat(p.Resources)}return p}function N(e){const t=e.replace(/&amp;/gi,"&");let n=i(t,R,["Id","Name"],"all");return null===n?[]:(n.forEach(e=>e.Id=parseInt(e.Id)),n.shift(),n)}function P(e){let t=i(e,S,["Id","Name"],"all");return null===t?[]:(t.forEach(e=>e.Id=parseInt(e.Id)),t.unshift({Id:0,Name:"全部"}),t)}function U(e,t,n){let r=[];const o=i(e,x.Resources,[],"all");return null===o?result:(o.forEach(e=>{let o=O(e);const a=t.split(" ").every(e=>o.Title.toLowerCase().includes(e.toLowerCase())),s=n.some(e=>o.PageUrl===e.PageUrl);a&&!s&&r.push(o)}),r)}function O(e){const t=i(e,x.Title,[]),n=i(e,x.TypeId,[]),r=i(e,x.TypeName,[]),o=i(e,x.SubgroupId,[]),s=i(e,x.SubgroupName,[]),u=i(e,x.Magnet,[]),l=i(e,x.PageUrl,[]),c=i(e,x.FileSize,[]),d=i(e,x.PublishDate,[]);return{Title:null===t?g:t.trim().replace(x.TitleReplacer,"$1"),TypeId:parseInt(n)||f,TypeName:r||y,SubgroupId:parseInt(o)||m,SubgroupName:s||w,Magnet:u||b,PageUrl:null===l?_:p+l,FileSize:c||I,PublishDate:null===d?v:a(d)}}var k=n(0);const M=`\n<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n  <meta charset="UTF-8" />\n  <meta name="viewport" content="width=device-width,initial-scale=1" />\n  <title>弹弹play资源搜索节点API - v${k.b}</title>\n</head>\n<body>\n  <h1>使用说明</h1>\n  <h2>GitHub - <a href="${k.a}">LussacZheng/dandanplay-resource-service</a></h2>\n</body>\n</html>\n`;const j=e=>t=>t.method.toLowerCase()===e.toLowerCase(),L=j("connect"),C=j("delete"),$=j("get"),E=j("head"),A=j("options"),D=j("patch"),z=j("post"),F=j("put"),H=j("trace"),J=e=>t=>{const n=new URL(encodeURI(t.url)).pathname;return(n.match(e)||[])[0]===n};var W=class{constructor(){this.routes=[]}handle(e,t){return this.routes.push({conditions:e,handler:t}),this}connect(e,t){return this.handle([L,J(e)],t)}delete(e,t){return this.handle([C,J(e)],t)}get(e,t){return this.handle([$,J(e)],t)}head(e,t){return this.handle([E,J(e)],t)}options(e,t){return this.handle([A,J(e)],t)}patch(e,t){return this.handle([D,J(e)],t)}post(e,t){return this.handle([z,J(e)],t)}put(e,t){return this.handle([F,J(e)],t)}trace(e,t){return this.handle([H,J(e)],t)}all(e){return this.handle([],e)}route(e){const t=this.resolve(e);return t?t.handler(e):new Response("resource not found",{status:404,statusText:"not found",headers:{"content-type":"text/plain"}})}resolve(e){return this.routes.find(t=>!(t.conditions&&(!Array.isArray(t)||t.conditions.length))||("function"==typeof t.conditions?t.conditions(e):t.conditions.every(t=>t(e))))}};async function Z(e){const t=new W;t.get("/subgroup",async()=>{const e=await async function(){return{Subgroups:N(await d(h.type_and_subgroup_url))}}();return new Response(JSON.stringify(e),u)}),t.get("/type",async()=>{const e=await async function(){return{Types:P(await d(h.type_and_subgroup_url))}}();return new Response(JSON.stringify(e),u)}),t.get("/list",async e=>{const t=await T(e);return new Response(JSON.stringify(t),u)}),t.get("/",async()=>new Response(await async function(){let e;try{e=await d("https://cdn.jsdelivr.net/gh/LussacZheng/dandanplay-resource-service@dist/web/index.html"),e=o(e,{VERSION:k.b})}catch(t){e=M}return e}(),l));return await t.route(e)}addEventListener("fetch",e=>e.respondWith(Z(e.request)))}]);

(其实你回复时不需要整段引用,直接评论回复,或只引用关键语句就行。这样可以减少无效信息。)

@LussacZheng
Copy link
Owner

v0.0.4-beta

v0.0.4-beta 现已发布,正式支持 "搜索指令" : $realtime , $page , $limit

获取

如果你感兴趣的话,可以试用并测试一下。有任何 Bug 或不合理的地方欢迎指出。

@zzz6839
Copy link
Author

zzz6839 commented Aug 21, 2021

感谢你对脚本的改进。

因为我只用到realtime 这个指令,所以测试beta版本了下是可以用的,建议加入简繁转换,可以更方便搜索出baha的三俗片源
比如奶子宿舍的管理員

@LussacZheng
Copy link
Owner

LussacZheng commented Aug 21, 2021

理由

cf-worker 版本暂时无法支持的原因如 文档 中所述:

理由是暂时未能实现一个轻量且全面的 JavaScript 简繁转换函数。而 go 实现没有此问题。

为 Node.js 编写的简繁转换库有很多,但直接引用过来会导致打包出的 worker.js 文件体积急剧增大,这可能会导致 "CPU 运行时间" 超出 Cloudflare Workers 免费账户 10 ms 的时间限制,或 其他限制

worker.js 的体积可能由 10 KB 增大到几百 KB 甚至几 MB 。而 golang 版本是编译后在服务器上运行的,其本身体积可达 15~20 MB ,再嵌入一个简繁体字符转换库,这对体积的影响微乎其微。

建议

我的建议是:对于 cf-worker 版本,用户可以分析目标资源的标题,从中提取出多个具有标识性的 “英文单词” 、“罗马音单词” 、“简繁体同形的字” 来作为关键词,并搭配组合,以规避这一问题。

以你所提到的《女神宿舍的管理员》为例,该番剧可能出现的名称有:

  • 女神宿舍的管理员
  • 女神宿舍的管理員
  • Megami-ryou no Ryoubo-kun

因此不妨将 女神ryoubo 作为搜索关键词,还可以带上 baha ,最终的关键词可以使用 女神 ryoubo baha $realtime

需求

如果你确实需要 cf-worker 版本支持简繁体转换,这里有一个我使用 chinese-conv 库完成简繁转换并打包成的临时版本

https://paste.ubuntu.com/p/NHcs6Xy35H/
https://gist.github.com/LussacZheng/30655835397714f4689960b2beb4fc0a

如你所见,由于内嵌了简繁体字符的转换字典,其代码长度急剧增加,因此我无法保证其稳定性。你可以尝试使用一段时间看看。

@zzz6839
Copy link
Author

zzz6839 commented Aug 21, 2021

感谢你的耐心回复,你的脚本已经有效的帮我解决了问题,本问题将关闭,期待你放出正式版和增加首页关于搜索指令的简要说明。
sticker

@zzz6839 zzz6839 closed this as completed Aug 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants