├── kikoplay.png ├── README.md ├── LICENSE ├── resource ├── nyaa.lua ├── comicat.lua ├── mikan.lua └── tpbsource.lua ├── danmu ├── mgtv.lua ├── tencent.lua ├── dandan.lua ├── acfun.lua ├── tucao.lua ├── ysjdm.lua ├── 5dm.lua ├── iqiyi.lua ├── youku.lua ├── bahamut.lua └── bilibili.lua ├── match └── dandan_match.lua ├── meta.json ├── bgm_calendar └── bgmlist.lua ├── library ├── bangumi.lua └── douban.lua └── reference.md /kikoplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/HEAD/kikoplay.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KikoPlay脚本仓库 2 | --- 3 | 这里是KikoPlay的脚本仓库 4 | KikoPlay支持Lua脚本,有5种类型: 5 | - 弹幕脚本: 位于script/danmu目录下,提供弹幕搜索、下载功能 6 | - 文件识别脚本:位于script/match目录下,提供文件识别功能,将文件识别到动画的某个剧集上,2.0.0新增 7 | - 资料脚本:位于script/library目录下,提供动画(或者其他类型的条目)搜索、详细信息获取、分集信息获取、标签获取功能 8 | - 资源脚本:位于script/resource目录下,提供资源搜索功能 9 | - 番组日历脚本:位于script/bgm_calendar,提供每日放送列表。0.8.2起新增 10 | 11 | 关于脚本开发的详细内容,请参考[KikoPlay脚本开发参考](reference.md) 12 | 13 | 提交改动时,如果是新脚本或者升级旧脚本,请注意修改`meta.json`文件 14 | ## 反馈 15 | 16 | 有新脚本可直接提交PR 17 | 18 | 如果有问题,创建issue或者联系我: 19 | dx_8820832#yeah.net(#→@),或者到QQ群874761809反馈 20 | 21 | ## 其他开发者提供的脚本 22 | 23 | - [TMDb](library/tmdb.lua):by kafovin, 从[themoviedb.org](themoviedb.org)获取信息,具体用法参考[这里](https://github.com/kafovin/KikoPlayScript) 24 | - [TVmazeList](bgm_calendar/tvmazelist.lua):by kafovin, 从 tvmaze.com 刮削剧集的日历时间表,具体用法参考[这里](https://github.com/kafovin/KikoPlayScript) 25 | - [TraktList](bgm_calendar/traktlist.lua):by kafovin, 从 trakt.tv 刮削媒体的日历时间表 26 | - [TPBsrc](resource\tpbsource.lua): TPBsource 资源脚本 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Protostars 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /resource/nyaa.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "Nyaa", 3 | ["id"] = "Kikyou.r.Nyaa", 4 | ["desc"] = "Nyaa搜索, nyaa.si", 5 | ["version"] = "0.1", 6 | } 7 | function search(keyword,page) 8 | --kiko_HttpGet arg: 9 | -- url: string 10 | -- query: table, {["key"]=value} value: string 11 | -- header: table, {["key"]=value} value: string 12 | local err,reply=kiko.httpget("https://nyaa.si/",{["f"]="0",["c"]="0_0",["q"]=keyword,["p"]=math.ceil(page)}) 13 | if err~=nil then error(err) end 14 | local content = reply["content"] 15 | local _,_,pageCount=string.find(content,"(%d+)%s*%s*
  • ") 16 | if pageCount==nil then 17 | pageCount=1 18 | end 19 | local rowPos=1 20 | local itemsList={} 21 | rowPos=string.find(content,"",rowPos,true) 22 | while rowPos~=nil do 23 | local _,cpos,url,title=string.find(content,".-",rowPos) 24 | url="https://nyaa.si" .. url 25 | title=string.gsub(title,"&","&") 26 | title=string.gsub(title,"⁢","<") 27 | title=string.gsub(title,">",">") 28 | title=string.gsub(title,""","\"") 29 | title=string.gsub(title," "," ") 30 | local _,cpos,magnet=string.find(content,"",cpos) 31 | magnet=string.gsub(magnet,"&","&") 32 | local _,cpos,size=string.find(content,"(.-)",cpos) 33 | local _,cpos,time=string.find(content,"(.-)",cpos) 34 | rowPos=cpos 35 | table.insert(itemsList,{ 36 | ["title"]=title, 37 | ["size"]=size, 38 | ["time"]=time, 39 | ["magnet"]=magnet, 40 | ["url"]=url 41 | }) 42 | rowPos=string.find(content,"",rowPos,true) 43 | end 44 | if rawlen(itemsList)==0 then 45 | pageCount=0 46 | end 47 | return itemsList, tonumber(pageCount) 48 | end -------------------------------------------------------------------------------- /resource/comicat.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "漫猫BT", 3 | ["id"] = "Kikyou.r.Comicat", 4 | ["desc"] = "漫猫BT搜索, www.comicat.org", 5 | ["version"] = "0.2", 6 | ["min_kiko"] = "2.0.0", 7 | } 8 | --return: 9 | -- errorInfo: nil/string 10 | -- pageCount: number 11 | -- searchResult: table, {Item} 12 | -- Item: table {"title"="","size"="","time"="","magnet"="",url=""} 13 | function search(keyword,page) 14 | local query = { 15 | ["keyword"]=keyword, 16 | ["page"]=page, 17 | } 18 | 19 | local b = kiko.browser.create() 20 | local succ = b:load("http://www.comicat.org/search.php", query) 21 | local content = b:html() 22 | 23 | local _, _, pageCount = string.find(content,".-共找到(%d+)条匹配资源") 24 | if pageCount == nil then 25 | -- 需要验证 26 | b:show("验证成功跳转后关闭窗口") 27 | content = b:html() 28 | end 29 | 30 | _, _, pageCount = string.find(content,".-共找到(%d+)条匹配资源") 31 | if pageCount==nil then 32 | error("Comicat WebPage Decode Failed") 33 | end 34 | if pageCount=="0" then 35 | return {}, 0 36 | end 37 | pageCount=math.ceil(pageCount/50) 38 | local spos,epos=string.find(content,".-") 39 | if spos==nil then 40 | error("Comicat WebPage Decode Failed") 41 | end 42 | local npos=spos 43 | local itemsList={} 44 | while npos%s*(.-)%s*",lnpos) 47 | local _,lnpos,hash,title=string.find(content,"%s*(.-)%s*",lnpos) 48 | title=string.gsub(title,"<.->"," ") 49 | title=string.gsub(title,"&","&") 50 | title=string.gsub(title,"⁢","<") 51 | title=string.gsub(title,">",">") 52 | title=string.gsub(title,""","\"") 53 | title=string.gsub(title," "," ") 54 | local _,lnpos,size=string.find(content,"%s*(.-)%s*",lnpos) 55 | local magnet="magnet:?xt=urn:btih:" .. hash .. "&tr=http://open.acgtracker.com:1096/announce" 56 | local url="http://www.comicat.org/show-" .. hash.. ".html" 57 | table.insert(itemsList,{ 58 | ["title"]=title, 59 | ["size"]=size, 60 | ["time"]=time, 61 | ["magnet"]=magnet, 62 | ["url"]=url 63 | }) 64 | local _,lnpos=string.find(content,"' ) 10 | str = string.gsub( str, '"', '"' ) 11 | str = string.gsub( str, ''', "'" ) 12 | str = string.gsub( str, '&#(%d+);', function(n) return utf8.char(n) end ) 13 | str = string.gsub( str, '&#x(%x+);', function(n) return utf8.char(tonumber(n,16)) end ) 14 | str = string.gsub( str, '&', '&' ) -- Be sure to do this after all others 15 | return str 16 | end 17 | --[[ 18 | local gsub, char = string.gsub, string.char 19 | local entityMap = {["lt"]="<",["gt"]=">",["amp"]="&",["quot"]='"',["apos"]="'"} 20 | local entitySwap = function(orig,n,s) 21 | return (n=='' and entityMap[s]) 22 | or (n=="#" and tonumber(s)) and string.char(s) 23 | or (n=="#x" and tonumber(s,16)) and string.char(tonumber(s,16)) 24 | or orig 25 | end 26 | function unescape(str) 27 | return (gsub( str, '(&(#?x?)([%d%a]+);)', entitySwap )) 28 | end 29 | --]] 30 | function search(keyword,page) 31 | --kiko_HttpGet arg: 32 | -- url: string 33 | -- query: table, {["key"]=value} value: string 34 | -- header: table, {["key"]=value} value: string 35 | local err,reply=kiko.httpget("https://mikanime.tv/Home/Search",{["searchstr"]=keyword}) 36 | if err~=nil then error(err) end 37 | local content = reply["content"] 38 | local rowPos=1 39 | local itemsList={} 40 | rowPos=string.find(content,"(.-)",rowPos) 43 | url="https://mikanani.me" .. url 44 | --title=string.gsub(title,"&","&") 45 | --title=string.gsub(title,"⁢","<") 46 | --title=string.gsub(title,">",">") 47 | --title=string.gsub(title,""","\"") 48 | --title=string.gsub(title," "," ") 49 | title=unescape(title) 50 | local _,cpos,magnet=string.find(content,"data%-clipboard%-text=\"(.-)\"",cpos) 51 | magnet=string.gsub(magnet,"&","&") 52 | local _,cpos,size=string.find(content,"%s*(.-)%s*",cpos) 53 | local _,cpos,time=string.find(content,"%s*(.-)%s*",cpos) 54 | rowPos=cpos 55 | table.insert(itemsList,{ 56 | ["title"]=title, 57 | ["size"]=size, 58 | ["time"]=time, 59 | ["magnet"]=magnet, 60 | ["url"]=url 61 | }) 62 | rowPos=string.find(content,"(.-)") 56 | if title == nil then title = "unknown" end 57 | 58 | table.insert(results, { 59 | ["title"] = title, 60 | ["data"] = data_str 61 | }) 62 | return results 63 | end 64 | 65 | function decodeDanmu(content, danmuList) 66 | local err, dmObj = kiko.json2table(content) 67 | if err ~= nil then return danmuList end 68 | local dmArray = dmObj["barrage_list"] 69 | if dmArray == nil then return danmuList end 70 | for _, dm in ipairs(dmArray) do 71 | local text = dm["content"] 72 | if string.startswith(text, "VIP :") then 73 | text = string.sub(text, 6) 74 | end 75 | local dmType = 0 76 | local color = 0xffffff 77 | local err, pobj = kiko.json2table(dm["content_style"]) 78 | if err==nil then 79 | local pos = tonumber(pobj["position"]) 80 | if pos~=nil then 81 | if pos==2 then --top 82 | dmType = 1 83 | elseif pos==3 then --bottom 84 | dmType = 2 85 | end 86 | end 87 | local pcolor = pobj["gradient_colors"] 88 | if type(pcolor) == "table" then 89 | pcolor = pcolor[1] 90 | end 91 | if type(pcolor) == "string" then 92 | color = tonumber(pcolor, 16) 93 | else 94 | color = tonumber(pcolor) 95 | end 96 | if color == nil then 97 | color = 0xffffff 98 | end 99 | end 100 | table.insert(danmuList, { 101 | ["text"]=text, 102 | ["time"]=tonumber(dm["time_offset"]), 103 | ["color"]=color, 104 | ["type"]=dmType, 105 | ["date"]=dm["create_time"], 106 | ["sender"]="[Tencent]" .. dm["nick"] 107 | }) 108 | end 109 | return danmuList 110 | end 111 | 112 | function downloadDanmu(id) 113 | local baseUrl = string.format("https://dm.video.qq.com/barrage/base/%s", id) 114 | local err, reply = kiko.httpget(baseUrl) 115 | if err ~= nil then error(err) end 116 | local err, obj = kiko.json2table(reply["content"]) 117 | if err ~= nil then 118 | kiko.log(reply["content"]) 119 | error(err) 120 | end 121 | local prefix = string.format("https://dm.video.qq.com/barrage/segment/%s/", id) 122 | local urls = {} 123 | for k, v in pairs(obj["segment_index"]) do 124 | table.insert(urls, prefix .. v["segment_name"]) 125 | end 126 | local danmuList = {} 127 | local _, rets = kiko.httpgetbatch(urls) 128 | for _, v in ipairs(rets) do 129 | if not v["hasError"] then 130 | danmuList = decodeDanmu(v["content"], danmuList) 131 | end 132 | end 133 | return danmuList 134 | end 135 | 136 | function danmu(source) 137 | local err, source_obj = kiko.json2table(source["data"]) 138 | if err ~= nil then error(err) end 139 | 140 | if source_obj["vid"] ~= nil then 141 | return nil, downloadDanmu(source_obj["vid"]) 142 | end 143 | 144 | local url = source_obj["url"] 145 | if url == nil then return nil, {} end 146 | local s, e = string.lastindexof(url, '/'), string.lastindexof(url, '.') 147 | if s == nil or e == nil then error("vid not found") end 148 | local vid = string.sub(url, s + 1, e - 1) 149 | if vid == nil or #vid == 0 then error("vid not found") end 150 | source_obj["vid"] = vid 151 | local _, data_str = kiko.table2json(source_obj) 152 | source["data"] = data_str 153 | return source, downloadDanmu(vid) 154 | end 155 | -------------------------------------------------------------------------------- /danmu/dandan.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "Dandan", 3 | ["id"] = "Kikyou.d.Dandan", 4 | ["desc"] = "弹弹Play弹幕脚本", 5 | ["version"] = "0.5", 6 | ["min_kiko"] = "2.0.0", 7 | ["label_color"] = "0x25AAE1", 8 | } 9 | 10 | 11 | settings = { 12 | ["withRelated"] = { 13 | ["title"] = "获取全部弹幕", 14 | ["desc"] = "获取全部来源的弹幕(包含弹弹之外的来源)", 15 | ["default"] = "n", 16 | ["choices"] = "y,n" 17 | }, 18 | ["appId"]={ 19 | ["title"]="AppId", 20 | ["desc"]="弹弹Play开放平台AppId", 21 | }, 22 | ["appSecret"]={ 23 | ["title"]="AppSecret", 24 | ["desc"]="弹弹Play开放平台AppSecret", 25 | }, 26 | } 27 | 28 | function hex2str(hex) 29 | local str = "" 30 | for i = 1, #hex, 2 do 31 | local byteStr = hex:sub(i, i + 1) 32 | local byte = tonumber(byteStr, 16) 33 | if byte then 34 | str = str .. string.char(byte) 35 | else 36 | error("Invalid hex string at position " .. i) 37 | end 38 | end 39 | return str 40 | end 41 | 42 | function get_header(path) 43 | local ts = os.time() 44 | local appid = settings["appId"] 45 | local appsecret = settings["appSecret"] 46 | local secret = string.format("%s%d%s%s", appid, ts, path, appsecret) 47 | local _, secret_hash = kiko.hashdata(secret, false, 0, 'sha256') 48 | local _, hash_base64 = kiko.base64(hex2str(secret_hash), 'to') 49 | return { 50 | ["Accept"] = "application/json", 51 | ["X-AppId"] = appid, 52 | ["X-Signature"] = hash_base64, 53 | ["X-Timestamp"] = ts 54 | } 55 | end 56 | 57 | function search(keyword) 58 | local query = { 59 | ["anime"]=keyword 60 | } 61 | 62 | local err, reply = nil, nil 63 | if kiko.envinfo()["kservice"] then 64 | local header = { 65 | ["Accept"] = "application/json" 66 | } 67 | local options = { 68 | ["set_dandan_header"] = true, 69 | ["dandan_path"] = "/api/v2/search/episodes", 70 | } 71 | err, reply = kiko.httpget("https://api.dandanplay.net/api/v2/search/episodes", query, header, options) 72 | else 73 | local header = get_header("/api/v2/search/episodes") 74 | err, reply = kiko.httpget("https://api.dandanplay.net/api/v2/search/episodes", query, header) 75 | end 76 | 77 | if err ~= nil then error(err) end 78 | local content = reply["content"] 79 | local err, obj = kiko.json2table(content) 80 | if err ~= nil then error(err) end 81 | local animes = obj["animes"] 82 | if animes == nil then return {} end 83 | local results = {} 84 | 85 | for _, item in ipairs(animes) do 86 | local animeTitle = item["animeTitle"] 87 | local eps = item["episodes"] 88 | if eps ~= nil then 89 | local _, data_str = kiko.table2json(eps) 90 | table.insert(results, { 91 | ["title"] = animeTitle, 92 | ["desc"] = string.format("共 %d 集", #eps), 93 | ["data"] = data_str 94 | }) 95 | end 96 | end 97 | return results 98 | end 99 | 100 | function epinfo(source) 101 | local err, eps = kiko.json2table(source["data"]) 102 | if err ~= nil then error(err) end 103 | local results = {} 104 | for _, ep in ipairs(eps) do 105 | table.insert(results, { 106 | ["title"] = ep["episodeTitle"], 107 | ["data"] = string.format("%d", ep["episodeId"]) 108 | }) 109 | end 110 | return results 111 | end 112 | 113 | function danmu(source) 114 | local query = {} 115 | local danmuUrl = "https://api.dandanplay.net/api/v2/comment/" .. source["data"] 116 | if settings["withRelated"] == 'y' then 117 | query["withRelated"] = "true" 118 | end 119 | 120 | local err, reply = nil, nil 121 | if kiko.envinfo()["kservice"] then 122 | local header = { 123 | ["Accept"] = "application/json" 124 | } 125 | local options = { 126 | ["set_dandan_header"] = true, 127 | ["dandan_path"] = "/api/v2/comment/" .. source["data"], 128 | } 129 | err, reply = kiko.httpget(danmuUrl, query, header, options) 130 | else 131 | local header = get_header("/api/v2/comment/" .. source["data"]) 132 | err, reply = kiko.httpget(danmuUrl, query, header) 133 | end 134 | 135 | if err ~= nil then error(err) end 136 | local danmuContent = reply["content"] 137 | 138 | local err, obj = kiko.json2table(danmuContent) 139 | local danmuArray = obj["comments"] 140 | if danmuArray == nil then return nil, {} end 141 | 142 | local danmus = {} 143 | for _, dmObj in ipairs(danmuArray) do 144 | local text = dmObj["m"] 145 | local attrs = string.split(dmObj["p"], ',') 146 | if #attrs >= 4 then 147 | local time = tonumber(attrs[1])*1000 148 | local mode = tonumber(attrs[2]) 149 | local dmType = 0 --rolling 150 | if mode == 4 then 151 | dmType = 2 --bottom 152 | elseif mode == 5 then 153 | dmType = 1 --top 154 | end 155 | local color = tonumber(attrs[3]) 156 | local sender = "[Dandan]" .. attrs[4] 157 | table.insert(danmus, { 158 | ["text"]=text, 159 | ["time"]=time, 160 | ["color"]=color, 161 | ["type"]=dmType, 162 | ["sender"]=sender 163 | }) 164 | end 165 | end 166 | return nil, danmus 167 | end 168 | -------------------------------------------------------------------------------- /match/dandan_match.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "弹弹Match", 3 | ["id"] = "Kikyou.m.DDMatch", 4 | ["desc"] = "弹弹Play动画关联脚本,利用弹弹Play API根据文件信息获取匹配的动画信息", 5 | ["version"] = "0.1", 6 | ["min_kiko"] = "2.0.0", 7 | } 8 | 9 | settings = { 10 | ["appId"]={ 11 | ["title"]="AppId", 12 | ["desc"]="弹弹Play开放平台AppId", 13 | }, 14 | ["appSecret"]={ 15 | ["title"]="AppSecret", 16 | ["desc"]="弹弹Play开放平台AppSecret", 17 | }, 18 | } 19 | 20 | function hex2str(hex) 21 | local str = "" 22 | for i = 1, #hex, 2 do 23 | local byteStr = hex:sub(i, i + 1) 24 | local byte = tonumber(byteStr, 16) 25 | if byte then 26 | str = str .. string.char(byte) 27 | else 28 | error("Invalid hex string at position " .. i) 29 | end 30 | end 31 | return str 32 | end 33 | 34 | function get_header(path) 35 | local ts = os.time() 36 | local appid = settings["appId"] 37 | local appsecret = settings["appSecret"] 38 | local secret = string.format("%s%d%s%s", appid, ts, path, appsecret) 39 | local _, secret_hash = kiko.hashdata(secret, false, 0, 'sha256') 40 | local _, hash_base64 = kiko.base64(hex2str(secret_hash), 'to') 41 | return { 42 | ["Accept"] = "application/json", 43 | ["X-AppId"] = appid, 44 | ["X-Signature"] = hash_base64, 45 | ["X-Timestamp"] = ts 46 | } 47 | end 48 | 49 | function setoption(key, val) 50 | kiko.log(string.format("Setting changed: %s = %s", key, val)) 51 | end 52 | 53 | function getEpInfo(epTitle) 54 | local _, _, index, epName = string.find(epTitle, "第(%d+)话%s*(.*)") 55 | if index ~= nil then 56 | return tonumber(index), 1, epName or "" 57 | end 58 | local _, _, index, epName = string.find(epTitle, "第(%d+)集%s*(.*)") 59 | if index ~= nil then 60 | return tonumber(index), 1, epName or "" 61 | end 62 | local _, _, index, epName = string.find(epTitle, "S(%d+)%s*(.*)") 63 | if index ~= nil then 64 | return tonumber(index), 2, epName or "" 65 | end 66 | return 1, 1, epTitle 67 | end 68 | 69 | function getFileTitle(path) 70 | local pos = 0 71 | for i = #path,1,-1 do 72 | local ch = string.sub(path, i, i) 73 | if ch == '\\' or ch == '/' then 74 | pos = i 75 | break 76 | end 77 | end 78 | local pos_dt = 0 79 | local titleExt = string.sub(path, pos+1) 80 | for i = 1,#titleExt do 81 | local ch = string.sub(titleExt, i, i) 82 | if ch == '.' then 83 | pos_dt = i 84 | break 85 | end 86 | end 87 | return string.sub(titleExt, 1, pos_dt-1) 88 | end 89 | 90 | bgmIdCache = {} 91 | function getBgmId(animeId) 92 | if bgmIdCache[animeId] ~= nil then 93 | return bgmIdCache[animeId] 94 | end 95 | 96 | local err, reply = nil, nil 97 | local path = "/api/v2/bangumi/" .. animeId 98 | if kiko.envinfo()["kservice"] then 99 | 100 | local header = { 101 | ["Accept"] = "application/json" 102 | } 103 | local options = { 104 | ["set_dandan_header"] = true, 105 | ["dandan_path"] = path, 106 | } 107 | err, reply = kiko.httpget("https://api.dandanplay.net" .. path, {}, header, options) 108 | else 109 | local header = get_header(path) 110 | err, reply = kiko.httpget("https://api.dandanplay.net" .. path, {}, header) 111 | end 112 | 113 | if err ~= nil then 114 | kiko.log(err) 115 | return nil 116 | end 117 | local content = reply["content"] 118 | local err, obj = kiko.json2table(content) 119 | if err ~= nil or obj["errorCode"] ~= 0 then 120 | kiko.log(err) 121 | return nil 122 | end 123 | local bgmPath = obj["bangumi"]["bangumiUrl"] 124 | kiko.log("BGM Path: " .. bgmPath) 125 | local bgmId = string.match(bgmPath, "https?://bangumi.tv/subject/(%d+)") 126 | if bgmId == nil then 127 | kiko.log("Failed to extract BGM ID from path: " .. bgmPath) 128 | bgmIdCache[animeId] = "" 129 | return nil 130 | end 131 | bgmIdCache[animeId] = bgmId 132 | return bgmId 133 | end 134 | 135 | function match(path) 136 | local err, fileHash = kiko.hashdata(path, true, 16*1024*1024) 137 | local post = { 138 | ["fileName"] = getFileTitle(path), 139 | ["fileSize"] = kiko.dir.fileinfo(path)["size"], 140 | ["fileHash"] = fileHash 141 | } 142 | local err, post_data = kiko.table2json(post) 143 | 144 | local err, reply = nil, nil 145 | if kiko.envinfo()["kservice"] then 146 | local header = { 147 | ["Content-Type"] = "application/json", 148 | ["Accept"] = "application/json" 149 | } 150 | local options = { 151 | ["set_dandan_header"] = true, 152 | ["dandan_path"] = "/api/v2/match", 153 | } 154 | err, reply = kiko.httppost("https://api.dandanplay.net/api/v2/match", post_data, header, {}, options) 155 | else 156 | local header = get_header("/api/v2/match") 157 | header["Content-Type"] = "application/json" 158 | err, reply = kiko.httppost("https://api.dandanplay.net/api/v2/match", post_data, header) 159 | end 160 | 161 | if err ~= nil then error(err) end 162 | local content = reply["content"] 163 | local err, obj = kiko.json2table(content) 164 | if err ~= nil then 165 | error(err) 166 | end 167 | if not obj["isMatched"] then 168 | return {["success"]=false}; 169 | end 170 | local matchObj = obj["matches"][1] 171 | if matchObj == nil then 172 | return {["success"]=false}; 173 | end 174 | local anime = matchObj["animeTitle"] 175 | local animeId = tostring(matchObj["animeId"]) 176 | local bgmId = getBgmId(animeId) 177 | local epIndex, epType, epName = getEpInfo(matchObj["episodeTitle"]) 178 | local match_res = { 179 | ["success"]=true, 180 | ["anime"]={ 181 | ["name"]=anime, 182 | }, 183 | ["ep"]={ 184 | ["name"]=epName, 185 | ["index"]=epIndex, 186 | ["type"]=epType 187 | } 188 | } 189 | if bgmId ~= nil and bgmId ~= "" then 190 | match_res["anime"]["data"] = bgmId 191 | match_res["anime"]["scriptId"] = "Kikyou.l.Bangumi" 192 | end 193 | return match_res 194 | end -------------------------------------------------------------------------------- /danmu/acfun.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "AcFun", 3 | ["id"] = "Kikyou.d.AcFun", 4 | ["desc"] = "AcFun弹幕脚本", 5 | ["version"] = "0.3", 6 | ["min_kiko"] = "2.0.0", 7 | ["label_color"] = "0xFD4C5D", 8 | } 9 | 10 | supportedURLsRe = { 11 | "(https?://)?www\\.acfun\\.cn/v/ac[0-9]+(_[0-9]+)?", 12 | "(https?://)?www\\.acfun\\.cn/bangumi/aa[0-9]+(_[0-9]+_[0-9]+)?" 13 | } 14 | 15 | sampleSupporedURLs = { 16 | "http://www.acfun.cn/v/ac4471456", 17 | "https://www.acfun.cn/bangumi/aa6000896" 18 | } 19 | 20 | function str2time(time_str) 21 | local timeArray = string.split(time_str, ':') 22 | local duration, base = 0, 0 23 | for i = #timeArray,1,-1 do 24 | duration = duration + 60^base*timeArray[i] 25 | base = base + 1 26 | end 27 | return duration 28 | end 29 | 30 | function search(keyword) 31 | local query = { 32 | ["keyword"]=keyword 33 | } 34 | local header = { 35 | 36 | } 37 | local err, reply = kiko.httpget("https://www.acfun.cn/rest/pc-direct/search/bgm", query, header) 38 | if err ~= nil then error(err) end 39 | local content = reply["content"] 40 | local err, obj = kiko.json2table(content) 41 | if err ~= nil then error(err) end 42 | local ret = obj["bgmList"] 43 | if ret == nil then return {} end 44 | local results = {} 45 | for _, item in ipairs(ret) do 46 | local itemType = item["itemType"] 47 | local bgmTitle = item["bgmTitle"] 48 | local bgmId = item["bgmId"] 49 | local i = 1 50 | local eps = {} 51 | for _, ep in ipairs(item["videoList"]) do 52 | local data = { 53 | ["url"] = string.format("https://www.acfun.cn/bangumi/aa%d_36188_%d", bgmId, ep["itemId"]), 54 | } 55 | local _, data_str = kiko.table2json(data) 56 | table.insert(eps, { 57 | ["title"] = bgmTitle .. " " .. tostring(i), 58 | ["data"] = data_str 59 | }) 60 | i = i+1 61 | end 62 | local _, data_str = kiko.table2json(eps) 63 | table.insert(results, { 64 | ["title"] = bgmTitle, 65 | ["desc"] = string.format("共 %d 集", #eps), 66 | ["data"] = data_str 67 | }) 68 | end 69 | return results 70 | end 71 | 72 | function epinfo(source) 73 | local err, obj = kiko.json2table(source["data"]) 74 | if err ~= nil then error(err) end 75 | return obj 76 | end 77 | 78 | function urlinfo(url) 79 | local pattens = { 80 | ["https?://www%.acfun%.cn/v/ac%d+"]="ac", 81 | ["www%.acfun%.cn/v/ac%d+"]="ac", 82 | ["https?://www%.acfun%.cn/bangumi/aa%d+"]="aa", 83 | ["www%.acfun%.cn/bangumi/aa%d+"]="aa" 84 | } 85 | local matched = nil 86 | for pv, k in pairs(pattens) do 87 | s, e = string.find(url, pv) 88 | if s then 89 | if e - s + 1 == #url then 90 | matched = k 91 | break 92 | end 93 | end 94 | end 95 | if matched == nil then error("不支持的URL") end 96 | local results = {} 97 | local data = { 98 | ["url"] = url 99 | } 100 | local _, data_str = kiko.table2json(data) 101 | table.insert(results, { 102 | ["title"] = "unknown", 103 | ["data"] = data_str 104 | }) 105 | return results 106 | end 107 | 108 | function downloadDanmu(vid) 109 | local danmuUrl = "https://www.acfun.cn/rest/pc-direct/new-danmaku/list" 110 | local headers = { 111 | ["Content-Type"]="application/x-www-form-urlencoded" 112 | } 113 | 114 | local pcursor = "1" 115 | local dm_arrays = {} 116 | while pcursor ~= nil and pcursor ~= "no_more" do 117 | local postdata = string.format("resourceId=%s&pcursor=%s&resourceType=9&count=200", vid, pcursor) 118 | local err, reply = kiko.httppost(danmuUrl, postdata, headers) 119 | if err ~= nil then 120 | kiko.log("slice fetch error: ", pcursor, err) 121 | pcursor = nil 122 | else 123 | local danmuContent = reply["content"] 124 | local err, obj = kiko.json2table(danmuContent) 125 | local danmuArray = obj["danmakus"] 126 | if danmuArray ~= nil then 127 | table.insert(dm_arrays, danmuArray) 128 | end 129 | pcursor = obj["pcursor"] 130 | end 131 | end 132 | 133 | local danmus = {} 134 | for _, danmuArray in ipairs(dm_arrays) do 135 | for _, dmObj in ipairs(danmuArray) do 136 | local text = dmObj["body"] 137 | local time = tonumber(dmObj["position"]) 138 | local mode = tonumber(dmObj["mode"]) 139 | local dmType = 0 --rolling 140 | if mode == 4 then 141 | dmType = 2 --bottom 142 | elseif mode == 5 then 143 | dmType = 1 --top 144 | end 145 | local size = tonumber(dmObj["size"]) 146 | if size == 18 then 147 | size = 1 148 | elseif size == 36 then 149 | size = 2 150 | else 151 | size = 0 152 | end 153 | local color = tonumber(dmObj["color"]) 154 | local date = tonumber(dmObj["createTime"])/1000 155 | local sender = "[AcFun]" .. string.format("%d", dmObj["userId"]) 156 | table.insert(danmus, { 157 | ["text"]=text, 158 | ["time"]=time, 159 | ["color"]=color, 160 | ["fontsize"]=size, 161 | ["type"]=dmType, 162 | ["date"]=date, 163 | ["sender"]=sender 164 | }) 165 | end 166 | end 167 | return danmus 168 | end 169 | 170 | function danmu(source) 171 | local err, source_obj = kiko.json2table(source["data"]) 172 | if err ~= nil then error(err) end 173 | 174 | if source_obj["vid"] ~= nil then 175 | return nil, downloadDanmu(source_obj["vid"]) 176 | end 177 | 178 | local url = source_obj["url"] 179 | if url == nil then return {} end 180 | local err, reply = kiko.httpget(url) 181 | if err ~= nil then error(err) end 182 | local content = reply["content"] 183 | 184 | local _, _, vid = string.find(content, "ideoId\":(%d+)") 185 | if vid == nil then error("视频Id解析失败") end 186 | source_obj["vid"] = vid 187 | local _, data_str = kiko.table2json(source_obj) 188 | source["data"] = data_str 189 | 190 | local _, _, durationMs = string.find(content, "\"durationMillis\":(%d+)") 191 | if durationMs ~= nil then source["duration"] = tonumber(durationMs)/1000 end 192 | 193 | local _, _, showTitle = string.find(content, "\"showTitle\":\"(..-)\"") 194 | if showTitle ~= nil then 195 | source["title"] = showTitle 196 | else 197 | local _, _, title = string.find(content, "\"title\":\"(..-)\"") 198 | if title ~= nil then source["title"] = title end 199 | end 200 | 201 | return source, downloadDanmu(vid) 202 | end 203 | -------------------------------------------------------------------------------- /danmu/tucao.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "Tucao", 3 | ["id"] = "Kikyou.d.Tucao", 4 | ["desc"] = "Tucao弹幕脚本", 5 | ["version"] = "0.3", 6 | ["min_kiko"] = "2.0.0", 7 | ["label_color"] = "0xF72B5F", 8 | } 9 | 10 | settings = { 11 | ["latest_addr"] = { 12 | ["title"] = "吐槽最新地址", 13 | ["desc"] = "地址不要添加'https://'前缀", 14 | ["default"] = "www.tucao.my", 15 | } 16 | } 17 | 18 | supportedURLsRe = { 19 | "(https?://)?www\\.tucao\\.my/play/h[0-9]+(#[0-9]+)?/?" 20 | } 21 | 22 | sampleSupporedURLs = { 23 | "https://www.tucao.my/play/h4077044/" 24 | } 25 | 26 | function search(keyword) 27 | local query = { 28 | ["m"]="content", 29 | ["c"]="search", 30 | ["a"]="init", 31 | ["catid"]="24", 32 | ["dosubmit"]="1", 33 | ["orderby"]="a.id+DESC", 34 | ["info%5Btitle%5D"]=keyword 35 | } 36 | local err, reply = kiko.httpget(string.format("https://%s/index.php", settings["latest_addr"]), query) 37 | if err ~= nil then error(err) end 38 | local content = reply["content"] 39 | local start = string.find(content, "
    ") 40 | if start == nil then return {} end 41 | 42 | local parser = kiko.htmlparser(content) 43 | parser:seekto(start-1) 44 | 45 | local results = {} 46 | while not parser:atend() do 47 | if parser:curproperty("class")=="t" then 48 | local href = parser:curproperty("href") 49 | local _, _, hid = string.find(href, "h(%d+)") 50 | local curTitle = parser:readcontent() 51 | if hid ~= nil and curTitle ~= nil then 52 | local data = { 53 | ["hid"] = hid, 54 | } 55 | local _, data_str = kiko.table2json(data) 56 | table.insert(results, { 57 | ["title"] = curTitle, 58 | ["data"] = data_str 59 | }) 60 | end 61 | end 62 | parser:readnext() 63 | end 64 | return results 65 | end 66 | 67 | function epinfo(source) 68 | local err, source_obj = kiko.json2table(source["data"]) 69 | if err ~= nil then error(err) end 70 | local baseUrl = string.format("http://%s/play/h%s/", settings["latest_addr"], source_obj["hid"]) 71 | local err, reply = kiko.httpget(baseUrl) 72 | if err ~= nil then error(err) end 73 | local content = reply["content"] 74 | local _, _, epContent = string.find(content, "
    • (.-)
    • .-
    ") 75 | local results = {} 76 | if epContent ~= nil then 77 | local eps = string.split(epContent, '|') 78 | local _, _, pageTitle = string.find(content, "

    (.-)<") 79 | local index = 0 80 | for i = 2,#eps do 81 | local title = string.split(eps[i], '*')[1] 82 | if title == nil or #title == 0 then 83 | if pageTitle == nil or #pageTitle == 0 then 84 | title = source["title"] 85 | else 86 | title = string.format("%s %d", pageTitle, index+1) 87 | end 88 | end 89 | local data = { 90 | ["hid"] = source_obj["hid"], 91 | ["index"] = tostring(index) 92 | } 93 | local _, data_str = kiko.table2json(data) 94 | index = index + 1 95 | table.insert(results, { 96 | ["title"] = title, 97 | ["data"] = data_str 98 | }) 99 | end 100 | end 101 | return results 102 | end 103 | 104 | function urlinfo(url) 105 | local base_url = string.gsub(settings["latest_addr"], "%.", "%%.") 106 | local pattens = { 107 | ["https?://" .. base_url .. "/play/h%d+/?"]="hid", 108 | [base_url .. "/play/h%d+/?"]="hid", 109 | ["https?://" .. base_url .. "/play/h%d+#%d+/?"]="hindex", 110 | [base_url .. "/play/h%d+#%d+/?"]="hindex", 111 | } 112 | local matched = nil 113 | for pv, k in pairs(pattens) do 114 | s, e = string.find(url, pv) 115 | if s then 116 | if e - s + 1 == #url then 117 | matched = k 118 | break 119 | end 120 | end 121 | end 122 | if matched == nil then error("不支持的URL") end 123 | local re = "h(%d+)" 124 | if matched == "hindex" then 125 | re = "h(%d+)#(%d+)" 126 | end 127 | local _, _, hid, index = string.find(url, re) 128 | if index == nil then index = "0" end 129 | local data = { 130 | ["hid"] = hid, 131 | ["index"] = index 132 | } 133 | local _, data_str = kiko.table2json(data) 134 | return epinfo({ 135 | ["title"]="", 136 | ["data"] = data_str 137 | }) 138 | end 139 | 140 | function danmu(source) 141 | local err, source_obj = kiko.json2table(source["data"]) 142 | if err ~= nil then error(err) end 143 | 144 | local query = { 145 | ["m"]="mukio", 146 | ["a"]="init", 147 | ["c"]="index", 148 | ["playerID"]=string.format("11-%s-1-%s", source_obj["hid"], source_obj["index"]) 149 | } 150 | local err, reply = kiko.httpget(string.format("http://%s/index.php", settings["latest_addr"]), query) 151 | if err ~= nil then error(err) end 152 | local content = reply["content"] 153 | 154 | local danmus = {} 155 | local xmlreader = kiko.xmlreader(content) 156 | while not xmlreader:atend() do 157 | if xmlreader:startelem() and xmlreader:name()=="d" then 158 | if xmlreader:hasattr("p") then 159 | local attrs = string.split(xmlreader:attr("p"), ',') 160 | if #attrs >= 5 then 161 | local text = xmlreader:elemtext() 162 | local time = tonumber(attrs[1]) * 1000 163 | local mode = tonumber(attrs[2]) 164 | local dmType = 0 --rolling 165 | if mode == 4 then 166 | dmType = 2 --bottom 167 | elseif mode == 5 then 168 | dmType = 1 --top 169 | end 170 | local size = tonumber(attrs[3]) 171 | if size == 18 then 172 | size = 1 173 | elseif size == 36 then 174 | size = 2 175 | else 176 | size = 0 177 | end 178 | local color = tonumber(attrs[4]) 179 | local date = attrs[5] 180 | local sender = "[Tucao]" 181 | table.insert(danmus, { 182 | ["text"]=text, 183 | ["time"]=time, 184 | ["color"]=color, 185 | ["fontsize"]=size, 186 | ["type"]=dmType, 187 | ["date"]=date, 188 | ["sender"]=sender 189 | }) 190 | end 191 | end 192 | end 193 | xmlreader:readnext() 194 | end 195 | return nil, danmus 196 | end -------------------------------------------------------------------------------- /danmu/ysjdm.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "异世界动漫", 3 | ["id"] = "Kikyou.d.ysjdm", 4 | ["desc"] = "异世界动漫弹幕脚本,www.dmmiku.com", 5 | ["version"] = "0.3.3", 6 | ["min_kiko"] = "2.0.0", 7 | ["label_color"] = "0xFF5C4B", 8 | } 9 | 10 | settings = { 11 | ["latest_addr"] = { 12 | ["title"] = "异世界动漫最新地址", 13 | ["desc"] = "地址不要添加'http://'前缀", 14 | ["default"] = "www.dmmiku.com", 15 | } 16 | } 17 | 18 | supportedURLsRe = { 19 | "(https?://)?www\\.dmmiku\\.com/index.php/vod/(detail|play)/id/\\d+(/sid/\\d+/nid/\\d+)?.html" 20 | } 21 | 22 | sampleSupporedURLs = { 23 | "https://www.dmmiku.com/index.php/vod/detail/id/1417.html", 24 | "https://www.dmmiku.com/index.php/vod/play/id/1559/sid/1/nid/1.html" 25 | } 26 | 27 | function search(keyword) 28 | local err, reply = kiko.httpget(string.format("https://%s/index.php/vod/search.html", settings["latest_addr"]), {["wd"]=keyword}) 29 | if err ~= nil then error(err) end 30 | local content = reply["content"] 31 | local parser = kiko.htmlparser(content) 32 | local spos, epos = string.find(content, "
    (.*)") 162 | if title ~= nil then source["title"] = title end 163 | end 164 | local _, _, player_info = string.find(content, "player_aaaa%s*=%s*(%{.-%})") 165 | if player_info == nil then error("视频信息解析失败: player_info") end 166 | local err, player_info_obj = kiko.json2table(player_info) 167 | if err ~= nil then error("视频信息解析失败: " .. err) end 168 | local video_url = player_info_obj["url"] 169 | if video_url == nil then error("视频信息解析失败: video_url") end 170 | 171 | local err, reply = kiko.httpget("https://bf.bfdm.xyz/m3u8.php", {["url"]=video_url}, {["Referer"]=string.format("https://%s/", settings["latest_addr"])}) 172 | if err ~= nil then error(err) end 173 | local content = reply["content"] 174 | local _, _, dm_id = string.find(content, "\"id\"%s*:%s*\"(.-)\",") 175 | if dm_id == nil then error("视频信息解析失败: dm_id") end 176 | 177 | source_obj["dm_id"] = dm_id 178 | local _, data_str = kiko.table2json(source_obj) 179 | source["data"] = data_str 180 | return source, downloadDanmu(source_obj["dm_id"]) 181 | end 182 | -------------------------------------------------------------------------------- /danmu/5dm.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "5dm", 3 | ["id"] = "Kikyou.d.5dm", 4 | ["desc"] = "5dm弹幕脚本", 5 | ["version"] = "0.3", 6 | ["min_kiko"] = "2.0.0", 7 | ["label_color"] = "0xEB5D56", 8 | } 9 | 10 | supportedURLsRe = { 11 | "(https?://)?www\\.5dm\\.link/(bangumi|end)/dv(\\d+)(\\?(link=[0-9]+)?)?", 12 | "(https?://)?www\\.5dm\\.link/(bangumi|end)/dv(\\d+)(\\?line=\\d+)&(link=[0-9]+)?" 13 | } 14 | 15 | sampleSupporedURLs = { 16 | "https://www.5dm.link/bangumi/dv56062?link=6", 17 | "https://www.5dm.link/end/dv17582?line=1&link=2" 18 | } 19 | 20 | function unescape(str) 21 | str = string.gsub( str, '<', '<' ) 22 | str = string.gsub( str, '>', '>' ) 23 | str = string.gsub( str, '"', '"' ) 24 | str = string.gsub( str, ''', "'" ) 25 | str = string.gsub( str, '&#(%d+);', function(n) return utf8.char(n) end ) 26 | str = string.gsub( str, '&#x(%x+);', function(n) return utf8.char(tonumber(n,16)) end ) 27 | str = string.gsub( str, '&', '&' ) -- Be sure to do this after all others 28 | return str 29 | end 30 | 31 | 32 | function epinfo(source) 33 | local err, source_obj = kiko.json2table(source["data"]) 34 | if err ~= nil then error(err) end 35 | 36 | local baseUrl = "https://www.5dm.link/bangumi/dv" .. source_obj["dv"] 37 | local headers = { 38 | ["User-Agent"] = kiko.browser.ua(), 39 | } 40 | local err, reply = kiko.httpget(baseUrl, {}, headers) 41 | if err ~= nil then error(err) end 42 | 43 | local content = reply["content"] 44 | local _, _, epContent = string.find(content, "(.-)") 45 | local results = {} 46 | if epContent ~= nil then 47 | local parser = kiko.htmlparser(epContent) 48 | while not parser:atend() do 49 | if parser:curnode()=="a" and parser:start() then 50 | local href = parser:curproperty("href") 51 | local _, _, dv, link = string.find(href, "dv(%d+)%?line=1&link=(%d+)") 52 | if dv ~= nil and link ~= nil then 53 | local data = { 54 | ["dv"] = dv, 55 | ["link"] = link 56 | } 57 | local _, data_str = kiko.table2json(data) 58 | title = parser:readuntil('a', false) 59 | title = string.gsub(title,"<.->", "") 60 | title = string.trim(title) 61 | table.insert(results, { 62 | ["title"] = title, 63 | ["data"] = data_str 64 | }) 65 | end 66 | end 67 | parser:readnext() 68 | end 69 | end 70 | return results 71 | end 72 | 73 | function urlinfo(url) 74 | local pattens = { 75 | ["https?://www%.5dm%.link/bangumi/dv(%d+)%?link=(%d+)"]="dv_link", 76 | ["www%.5dm%.link/bangumi/dv(%d+)%?link=(%d+)"]="dv_link", 77 | ["https?://www%.5dm%.link/bangumi/dv(%d+)%?line=%d+&link=(%d+)"]="dv_link", 78 | ["www%.5dm%.link/bangumi/dv(%d+)%?line=%d+&link=(%d+)"]="dv_link", 79 | ["https?://www%.5dm%.link/bangumi/dv(%d+)"]="dv", 80 | ["www%.5dm%.link/bangumi/dv(%d+)"]="dv", 81 | ["https?://www%.5dm%.link/end/dv(%d+)%?link=(%d+)"]="dv_link", 82 | ["www%.5dm%.link/end/dv(%d+)%?link=(%d+)"]="dv_link", 83 | ["https?://www%.5dm%.link/end/dv(%d+)%?line=%d+&link=(%d+)"]="dv_link", 84 | ["www%.5dm%.link/end/dv(%d+)%?line=%d+&link=(%d+)"]="dv_link", 85 | ["https?://www%.5dm%.link/end/dv(%d+)"]="dv", 86 | ["www%.5dm%.link/end/dv(%d+)"]="dv", 87 | } 88 | local matched = nil 89 | for pv, k in pairs(pattens) do 90 | s, e = string.find(url, pv) 91 | if s then 92 | if e - s + 1 == #url then 93 | matched = k 94 | break 95 | end 96 | end 97 | end 98 | if matched == nil then error("不支持的URL") end 99 | local _, _, dv = string.find(url, "dv(%d+)") 100 | local data = { 101 | ["dv"] = dv 102 | } 103 | if matched == "dv_link" then 104 | local _, _, link = string.find(url, "link=(%d+)") 105 | data["link"] = link 106 | end 107 | local _, data_str = kiko.table2json(data) 108 | return epinfo({ 109 | ["data"] = data_str 110 | }) 111 | end 112 | 113 | function downloadDanmu(cid) 114 | local danmuUrl = "https://www.5dm.link/player/nxml.php" 115 | local query = { 116 | ["id"] = cid 117 | } 118 | local headers = { 119 | ["Accept"]="application/json", 120 | ["User-Agent"] = kiko.browser.ua(), 121 | } 122 | 123 | local err, reply = kiko.httpget(danmuUrl, query, headers) 124 | if err ~= nil then error(err) end 125 | 126 | local danmuContent = reply["content"] 127 | local err, obj = kiko.json2table(danmuContent) 128 | local danmuArray = obj["data"] 129 | 130 | local danmus = {} 131 | 132 | for _, dmObj in ipairs(danmuArray) do 133 | local time = tonumber(dmObj[1]) * 1000 134 | 135 | local dmType = 0 136 | if dmObj[2] == "top" then 137 | dmType = 1 138 | elseif dmObj[2] == "bottom" then 139 | dmType = 2 140 | end 141 | 142 | local color = dmObj[3] 143 | if color == "#fff" then 144 | color = 0xffffff 145 | else 146 | if string.sub(color, 1, 1) == "#" then 147 | color = tonumber(string.sub(color, 2), 16) 148 | else 149 | if string.sub(color, 1, 3) == "rgb" then 150 | color = string.sub(color, 5, -2) 151 | color = string.gsub(color, " ", "") 152 | local r, g, b = string.match(color, "(%d+),(%d+),(%d+)") 153 | color = tonumber(r) << 16 | tonumber(g) << 8 | tonumber(b) 154 | end 155 | end 156 | end 157 | 158 | local sender = tostring(dmObj[4]) 159 | 160 | local text = unescape(dmObj[5]) 161 | if text == "文明追番,请勿剧透!" then 162 | goto continue 163 | end 164 | 165 | table.insert(danmus, { 166 | ["text"]=text, 167 | ["time"]=time, 168 | ["color"]=color, 169 | ["fontsize"]=1, 170 | ["type"]=dmType, 171 | ["sender"]=sender 172 | }) 173 | 174 | ::continue:: 175 | end 176 | return danmus 177 | end 178 | 179 | function danmu(source) 180 | local err, source_obj = kiko.json2table(source["data"]) 181 | if err ~= nil then error(err) end 182 | 183 | if source_obj["cid"] ~= nil then 184 | return nil, downloadDanmu(source_obj["cid"]) 185 | end 186 | 187 | local url = "https://www.5dm.link/bangumi/dv" .. source_obj["dv"] 188 | local query = {} 189 | if source_obj["link"] ~= nil then 190 | query['line'] = "1" 191 | query["link"] = source_obj["link"] 192 | end 193 | local headers = { 194 | ["User-Agent"] = kiko.browser.ua(), 195 | } 196 | local err, reply = kiko.httpget(url, query, headers) 197 | 198 | if err ~= nil then error(err) end 199 | local content = reply["content"] 200 | 201 | local _, _, cid = string.find(content, "cid=(.-)&") 202 | if cid == nil then error("cid解析失败") end 203 | source_obj["cid"] = cid 204 | local _, data_str = kiko.table2json(source_obj) 205 | source["data"] = data_str 206 | 207 | return source, downloadDanmu(cid) 208 | end 209 | -------------------------------------------------------------------------------- /danmu/iqiyi.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "爱奇艺", 3 | ["id"] = "Kikyou.d.Iqiyi", 4 | ["desc"] = "爱奇艺弹幕脚本", 5 | ["version"] = "0.4", 6 | ["min_kiko"] = "2.0.0", 7 | ["label_color"] = "0x00da5a", 8 | } 9 | 10 | supportedURLsRe = { 11 | "(https?://)?www\\.iqiyi\\.com/(v|w)_.+\\.html" 12 | } 13 | 14 | sampleSupporedURLs = { 15 | "https://www.iqiyi.com/v_19rrofvlmo.html", 16 | } 17 | 18 | function str2time(time_str) 19 | local timeArray = string.split(time_str, ':') 20 | local duration, base = 0, 0 21 | for i = #timeArray,1,-1 do 22 | duration = duration + 60^base*timeArray[i] 23 | base = base + 1 24 | end 25 | return duration 26 | end 27 | 28 | function search(keyword) 29 | local query = { 30 | ["key"] = keyword, 31 | ["if"] = "html5", 32 | } 33 | local err, reply = kiko.httpget("https://search.video.iqiyi.com/o", query) 34 | if err ~= nil then error(err) end 35 | local err, obj = kiko.json2table(reply.content) 36 | if err ~= nil then error(err) end 37 | 38 | local res = {} 39 | if obj["data"] == nil or obj["data"]["docinfos"] == nil then 40 | return res 41 | end 42 | 43 | for _, doc in ipairs(obj["data"]["docinfos"]) do 44 | local albumDocInfo = doc["albumDocInfo"] 45 | if albumDocInfo ~= nil and albumDocInfo["videoinfos"] ~= nil then 46 | local items = {} 47 | for _, video in ipairs(albumDocInfo["videoinfos"]) do 48 | if video["tvId"] ~= nil then 49 | local source_obj = { 50 | ["vid"] = string.format("%d", video["tvId"]), 51 | ["pieces"] = math.ceil(video["timeLength"] / 300 + 1), 52 | } 53 | local _, data_str = kiko.table2json(source_obj) 54 | local source = { 55 | ["title"] = video["itemshortTitle"], 56 | ["duration"] = video["timeLength"], 57 | ["data"] = data_str, 58 | } 59 | table.insert(items, source) 60 | end 61 | end 62 | if #items == 1 then 63 | table.insert(res, items[1]) 64 | elseif #items > 1 then 65 | local _, data_str = kiko.table2json(items) 66 | table.insert(res, { 67 | ["title"] = albumDocInfo["albumTitle"], 68 | ["desc"] = string.format("共 %d 集", #items), 69 | ["data"] = data_str 70 | }) 71 | end 72 | end 73 | end 74 | 75 | return res 76 | end 77 | 78 | function epinfo(source) 79 | local err, obj = kiko.json2table(source["data"]) 80 | if err ~= nil then error(err) end 81 | if obj["url"] ~= nil then 82 | return {source} 83 | elseif obj["vid"] ~= nil then 84 | return {source} 85 | end 86 | return obj 87 | end 88 | 89 | function urlinfo(url) 90 | local pattens = { 91 | ["https?://www%.iqiyi%.com/v_.+%.html"]="iqy", 92 | ["https?://www%.iqiyi%.com/w_.+%.html"]="iqy", 93 | ["www%.iqiyi%.com/v_.+%.html"]="iqy", 94 | ["www%.iqiyi%.com/w_.+%.html"]="iqy" 95 | } 96 | local matched = nil 97 | for pv, k in pairs(pattens) do 98 | s, e = string.find(url, pv) 99 | if s then 100 | if e - s + 1 == #url then 101 | matched = k 102 | break 103 | end 104 | end 105 | end 106 | if matched == nil then error("不支持的URL") end 107 | local results = {} 108 | local data = { ["url"] = url } 109 | local _, data_str = kiko.table2json(data) 110 | table.insert(results, { 111 | ["title"] = "unknown", 112 | ["data"] = data_str 113 | }) 114 | return results 115 | end 116 | 117 | function decodeDanmu(content, danmuList) 118 | local err, danmuContent = kiko.decompress(content) 119 | if err ~= nil then return danmuList end 120 | local xmlreader = kiko.xmlreader(danmuContent) 121 | local curDate, curText, curTime, curColor, curUID = nil, nil, nil, nil, nil 122 | while not xmlreader:atend() do 123 | if xmlreader:startelem() then 124 | if xmlreader:name()=="contentId" then 125 | curDate = string.sub(xmlreader:elemtext(), 1, 10) 126 | elseif xmlreader:name()=="content" then 127 | curText = xmlreader:elemtext() 128 | elseif xmlreader:name()=="showTime" then 129 | curTime = tonumber(xmlreader:elemtext()) * 1000 130 | elseif xmlreader:name()=="color" then 131 | curColor = tonumber(xmlreader:elemtext(), 16) 132 | elseif xmlreader:name()=="uid" then 133 | curUID = "[iqiyi]" .. xmlreader:elemtext() 134 | end 135 | elseif xmlreader:endelem() then 136 | if xmlreader:name()=="bulletInfo" then 137 | table.insert(danmuList, { 138 | ["text"]=curText, 139 | ["time"]=curTime, 140 | ["color"]=curColor, 141 | ["date"]=curDate, 142 | ["sender"]=curUID 143 | }) 144 | end 145 | end 146 | xmlreader:readnext() 147 | end 148 | return danmuList 149 | end 150 | 151 | function downloadDanmu(id, pieces) 152 | local tvid = "0000" .. id 153 | local s1 = string.sub(tvid, -4, -3) 154 | local s2 = string.sub(tvid, -2) 155 | local url = string.format("http://cmts.iqiyi.com/bullet/%s/%s/%s_300_%s.z", s1, s2, id, "%d") 156 | local danmuList = {} 157 | if pieces == nil then 158 | local i = 1 159 | while true do 160 | local err, reply = kiko.httpget(string.format(url, i)) 161 | if err ~= nil then break end 162 | local content = reply["content"] 163 | i = i+1 164 | danmuList = decodeDanmu(content, danmuList) 165 | end 166 | else 167 | local urls = {} 168 | for i=1,pieces do 169 | table.insert(urls, string.format(url, i)) 170 | end 171 | local _, rets = kiko.httpgetbatch(urls) 172 | for _, v in ipairs(rets) do 173 | if not v["hasError"] then 174 | danmuList = decodeDanmu(v["content"], danmuList) 175 | end 176 | end 177 | end 178 | return danmuList 179 | end 180 | 181 | function danmu(source) 182 | local err, source_obj = kiko.json2table(source["data"]) 183 | if err ~= nil then error(err) end 184 | 185 | if source_obj["vid"] ~= nil then 186 | return nil, downloadDanmu(source_obj["vid"], source_obj["pieces"]) 187 | end 188 | 189 | 190 | local headers = { 191 | ["Referer"] = source_obj["url"], 192 | } 193 | 194 | local err, reply = kiko.httpget("https://mesh.if.iqiyi.com/player/lw/lwplay/accelerator.js", {["format"]="json"}, headers) 195 | if err ~= nil then error(err) end 196 | local err, obj = kiko.json2table(reply.content) 197 | if err ~= nil then error(err) end 198 | 199 | local videoInfo = obj["videoInfo"] 200 | if videoInfo == nil or videoInfo["tvId"] == nil then 201 | error("视频信息获取失败") 202 | end 203 | 204 | local err, reply = kiko.httpget(string.format("https://pcw-api.iqiyi.com/video/video/baseinfo/%d", videoInfo["tvId"])) 205 | if err ~= nil then error(err) end 206 | local err, obj = kiko.json2table(reply.content) 207 | if err ~= nil then error(err) end 208 | local data_obj = obj["data"] 209 | 210 | source["title"] = videoInfo["title"] 211 | source["duration"] = str2time(data_obj["duration"]) 212 | source_obj["vid"] = string.format("%d", videoInfo["tvId"]) 213 | source_obj["pieces"] = math.ceil(source["duration"] / 300 + 1) 214 | local _, data_str = kiko.table2json(source_obj) 215 | source["data"] = data_str 216 | return source, downloadDanmu(source_obj["vid"], source_obj["pieces"]) 217 | end 218 | -------------------------------------------------------------------------------- /meta.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "desc": "AcFun弹幕脚本", 4 | "id": "Kikyou.d.AcFun", 5 | "min_kiko": "2.0.0", 6 | "name": "AcFun", 7 | "type": "danmu", 8 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/acfun.lua", 9 | "version": "0.3" 10 | }, 11 | { 12 | "desc": "巴哈姆特动画疯弹幕脚本", 13 | "id": "Kikyou.d.Gamer", 14 | "min_kiko": "2.0.0", 15 | "name": "动画疯", 16 | "type": "danmu", 17 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/bahamut.lua", 18 | "version": "0.2" 19 | }, 20 | { 21 | "desc": "Bilibili弹幕脚本", 22 | "id": "Kikyou.d.Bilibili", 23 | "min_kiko": "2.0.0", 24 | "name": "Bilibili", 25 | "type": "danmu", 26 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/bilibili.lua", 27 | "version": "0.3.2" 28 | }, 29 | { 30 | "desc": "弹弹Play弹幕脚本", 31 | "id": "Kikyou.d.Dandan", 32 | "min_kiko": "2.0.0", 33 | "name": "Dandan", 34 | "type": "danmu", 35 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/dandan.lua", 36 | "version": "0.5" 37 | }, 38 | { 39 | "desc": "爱奇艺弹幕脚本", 40 | "id": "Kikyou.d.Iqiyi", 41 | "min_kiko": "2.0.0", 42 | "name": "爱奇艺", 43 | "type": "danmu", 44 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/iqiyi.lua", 45 | "version": "0.4" 46 | }, 47 | { 48 | "desc": "芒果TV弹幕脚本", 49 | "id": "Kikyou.d.mgtv", 50 | "min_kiko": "2.0.0", 51 | "name": "芒果TV", 52 | "type": "danmu", 53 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/mgtv.lua", 54 | "version": "0.2" 55 | }, 56 | { 57 | "desc": "腾讯视频弹幕脚本", 58 | "id": "Kikyou.d.Tencent", 59 | "min_kiko": "2.0.0", 60 | "name": "腾讯视频", 61 | "type": "danmu", 62 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/tencent.lua", 63 | "version": "0.4" 64 | }, 65 | { 66 | "desc": "Tucao弹幕脚本", 67 | "id": "Kikyou.d.Tucao", 68 | "min_kiko": "2.0.0", 69 | "name": "Tucao", 70 | "type": "danmu", 71 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/tucao.lua", 72 | "version": "0.3" 73 | }, 74 | { 75 | "desc": "优酷弹幕脚本", 76 | "id": "Kikyou.d.youku", 77 | "min_kiko": "2.0.0", 78 | "name": "优酷", 79 | "type": "danmu", 80 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/youku.lua", 81 | "version": "0.2" 82 | }, 83 | { 84 | "desc": "5dm弹幕脚本,www.5dm.link", 85 | "id": "Kikyou.d.5dm", 86 | "min_kiko": "2.0.0", 87 | "name": "5dm", 88 | "type": "danmu", 89 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/5dm.lua", 90 | "version": "0.3" 91 | }, 92 | { 93 | "desc": "异世界动漫弹幕脚本,www.dmmiku.com", 94 | "id": "Kikyou.d.ysjdm", 95 | "min_kiko": "2.0.0", 96 | "name": "异世界动漫", 97 | "type": "danmu", 98 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/danmu/ysjdm.lua", 99 | "version": "0.3.3" 100 | }, 101 | { 102 | "desc": "Bangumi脚本,从bgm.tv中获取动画信息", 103 | "id": "Kikyou.l.Bangumi", 104 | "min_kiko": "0.9.1", 105 | "name": "Bangumi", 106 | "type": "library", 107 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/library/bangumi.lua", 108 | "version": "0.3.1" 109 | }, 110 | { 111 | "desc": "弹弹Play动画识别脚本,利用弹弹Play API根据文件信息获取匹配的动画信息", 112 | "id": "Kikyou.m.DDMatch", 113 | "min_kiko": "2.0.0", 114 | "name": "弹弹Match", 115 | "type": "match", 116 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/library/dandan_match.lua", 117 | "version": "0.1" 118 | }, 119 | { 120 | "desc": "TMDb+ 文件识别脚本", 121 | "id": "Kikyou.m.TMDb", 122 | "min_kiko": "2.0.0", 123 | "name": "TMDB文件识别", 124 | "type": "match", 125 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/library/tmdb_match.lua", 126 | "version": "0.2.29" 127 | }, 128 | { 129 | "desc": "豆瓣电影脚本,从 movie.douban.com 中获取视频信息", 130 | "id": "Kikyou.l.Douban", 131 | "min_kiko": "2.0.0", 132 | "name": "豆瓣电影", 133 | "type": "library", 134 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/library/douban.lua", 135 | "version": "0.2" 136 | }, 137 | { 138 | "desc": "TMDb+ 资料刮削脚本 - Edited by: kafovin \n从 The Movie Database (TMDb) 刮削影剧元数据,也可设置选择刮削fanart的媒体图片、Jellyfin/Emby的本地元数据、TVmaze的剧集演员。", 139 | "id": "Kikyou.l.TMDb", 140 | "min_kiko": "0.9.1", 141 | "name": "TMDb+Lib", 142 | "type": "library", 143 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/library/tmdb.lua", 144 | "version": "0.2.29" 145 | }, 146 | { 147 | "desc": "漫猫BT搜索, www.comicat.org", 148 | "id": "Kikyou.r.Comicat", 149 | "min_kiko": "2.0.0", 150 | "name": "漫猫BT", 151 | "type": "resource", 152 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/resource/comicat.lua", 153 | "version": "0.2" 154 | }, 155 | { 156 | "desc": "Mikan Project资源搜索, mikanani.me", 157 | "id": "Kikyou.r.Mikan", 158 | "min_kiko": "", 159 | "name": "Mikan", 160 | "type": "resource", 161 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/resource/mikan.lua", 162 | "version": "0.2" 163 | }, 164 | { 165 | "desc": "Nyaa搜索, nyaa.si", 166 | "id": "Kikyou.r.Nyaa", 167 | "min_kiko": "", 168 | "name": "Nyaa", 169 | "type": "resource", 170 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/resource/nyaa.lua", 171 | "version": "0.1" 172 | }, 173 | { 174 | "desc": "TPBsource 资源信息脚本(测试中,不稳定) Edited by: anonymous\n从 thePirateBay 刮削媒体资源信息。", 175 | "id": "Kikyou.r.TPBsource", 176 | "min_kiko": "0.9.1", 177 | "name": "TPBsrc", 178 | "type": "resource", 179 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/resource/tpbsource.lua", 180 | "version": "0.0.05" 181 | }, 182 | { 183 | "desc": "bgmlist 番组日历", 184 | "id": "Kikyou.b.Bgmlist", 185 | "min_kiko": "", 186 | "name": "bgmlist", 187 | "type": "bgm_calendar", 188 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/bgm_calendar/bgmlist.lua", 189 | "version": "0.1" 190 | }, 191 | { 192 | "desc": "Trakt 媒体日历脚本(测试中,不稳定) Edited by: kafovin \n从 trakt.tv 刮削媒体的日历时间表,可在日历中自动标记Trakt账户里已关注的媒体。", 193 | "id": "Kikyou.b.TraktList", 194 | "min_kiko": "0.9.1", 195 | "name": "TraktList", 196 | "type": "bgm_calendar", 197 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/bgm_calendar/traktlist.lua", 198 | "version": "0.1.06" 199 | }, 200 | { 201 | "desc": "TVmaze 剧集日历脚本(测试中,不稳定) Edited by: kafovin \n从 tvmaze.com 刮削剧集的日历时间表。", 202 | "id": "Kikyou.b.TVmazeList", 203 | "min_kiko": "0.9.1", 204 | "name": "TVmazeList", 205 | "type": "bgm_calendar", 206 | "url": "https://raw.githubusercontent.com/KikoPlayProject/KikoPlayScript/master/bgm_calendar/traktlist.lua", 207 | "version": "0.1.22" 208 | } 209 | ] 210 | -------------------------------------------------------------------------------- /bgm_calendar/bgmlist.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "BgmList", 3 | ["id"] = "Kikyou.b.Bgmlist", 4 | ["desc"] = "bgmlist 番组日历", 5 | ["version"] = "0.1", 6 | } 7 | 8 | settings = { 9 | ["load_last_season"]={ 10 | ["title"]="加载上一季未完结动画", 11 | ["default"]="y", 12 | ["desc"]="在当季番组中添加上一季未完结动画", 13 | ["choices"]="y,n" 14 | } 15 | } 16 | 17 | site_json = [[{"acfun":{"title":"AcFun","urlTemplate":"https://www.acfun.cn/bangumi/aa{{id}}","regions":["CN"],"type":"onair"},"bilibili":{"title":"哔哩哔哩","urlTemplate":"https://www.bilibili.com/bangumi/media/md{{id}}/","regions":["CN"],"type":"onair"},"bilibili_hk_mo_tw":{"title":"哔哩哔哩(港澳台)","urlTemplate":"https://www.bilibili.com/bangumi/media/md{{id}}/","regions":["HK","MO","TW"],"type":"onair"},"sohu":{"title":"搜狐视频","urlTemplate":"https://tv.sohu.com/{{id}}","regions":["CN"],"type":"onair"},"youku":{"title":"优酷","urlTemplate":"https://list.youku.com/show/id_z{{id}}.html","regions":["CN"],"type":"onair"},"qq":{"title":"腾讯视频","urlTemplate":"https://v.qq.com/detail/{{id}}.html","regions":["CN"],"type":"onair"},"iqiyi":{"title":"爱奇艺","urlTemplate":"https://www.iqiyi.com/{{id}}.html","regions":["CN"],"type":"onair"},"letv":{"title":"乐视","urlTemplate":"https://www.le.com/comic/{{id}}.html","regions":["CN"],"type":"onair"},"pptv":{"title":"PPTV","urlTemplate":"http://v.pptv.com/page/{{id}}.html","regions":["CN"],"type":"onair"},"mgtv":{"title":"芒果tv","urlTemplate":"https://www.mgtv.com/h/{{id}}.html","regions":["CN"],"type":"onair"},"nicovideo":{"title":"Niconico","urlTemplate":"https://ch.nicovideo.jp/{{id}}","regions":["JP"],"type":"onair"},"netflix":{"title":"Netflix","urlTemplate":"https://www.netflix.com/title/{{id}}","type":"onair"},"gamer":{"title":"動畫瘋","urlTemplate":"https://acg.gamer.com.tw/acgDetail.php?s={{id}}","regions":["TW"],"type":"onair"},"muse_hk":{"title":"木棉花 HK","urlTemplate":"https://www.youtube.com/playlist?list={{id}}","regions":["HK","MO"],"type":"onair"},"ani_one_asia":{"title":"Ani-One Asia","urlTemplate":"https://www.youtube.com/playlist?list={{id}}","regions":["HK","TW","MO","SG","MY","PH","TH","ID","VN","KH","BD","BN","BT","FJ","FM","IN","KH","LA","LK","MH","MM","MN","MV","NP","NR","PG","PK","PW","SB","TL","TO","TV","VU","WS"],"type":"onair"},"viu":{"title":"Viu","urlTemplate":"https://www.viu.com/ott/hk/zh-hk/vod/{{id}}/","regions":["HK","SG","MY","IN","PH","TH","MM","BH","EG","JO","KW","OM","QA","SA","AE","ZA"],"type":"onair"}}]] 18 | _, site_map = kiko.json2table(site_json) 19 | 20 | function getseason() 21 | local err, reply = kiko.httpget("https://bgmlist.com/api/v1/bangumi/season") 22 | if err ~= nil then error(err) end 23 | local content = reply["content"] 24 | local err, obj = kiko.json2table(content) 25 | if err ~= nil then 26 | error(err) 27 | end 28 | local seasons = {} 29 | month_tb = { 30 | ["q1"]="01", 31 | ["q2"]="04", 32 | ["q3"]="07", 33 | ["q4"]="10" 34 | } 35 | for _, season in pairs(obj["items"]) do 36 | local year = string.sub(season, 1, 4) 37 | local month = month_tb[string.sub(season, 5, 6)] 38 | table.insert(seasons, { 39 | ["title"]=string.format("%s-%s", year, month) 40 | }) 41 | end 42 | return seasons 43 | end 44 | 45 | function timestamp(dateStringArg) 46 | local inYear, inMonth, inDay, inHour, inMinute, inSecond, inZone = 47 | string.match(dateStringArg, '^(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d):(%d%d)(.-)$') 48 | local zHours, zMinutes = string.match(inZone, '^(.-):(%d%d)$') 49 | if tonumber(inYear) < 1970 then 50 | return 0 51 | end 52 | local returnTime = os.time({year=inYear, month=inMonth, day=inDay, hour=inHour, min=inMinute, sec=inSecond, isdst=false}) 53 | if zHours then 54 | returnTime = returnTime - ((tonumber(zHours)*3600) + (tonumber(zMinutes)*60)) 55 | end 56 | return returnTime 57 | end 58 | 59 | function isnew(syear, smonth, ts) 60 | if tonumber(syear) < 1970 then 61 | return true 62 | end 63 | local start = os.time({year=tonumber(syear), month=tonumber(smonth), day=1, hour=0, isdst=false}) 64 | return ts >= start 65 | end 66 | 67 | function getbgmlist(season) 68 | local season_title = season["title"] 69 | local year = string.sub(season_title, 1, 4) 70 | local month = string.sub(season_title, 6, 7) 71 | local q_tb = { 72 | ["01"]="q1", 73 | ["04"]="q2", 74 | ["07"]="q3", 75 | ["10"]="q4" 76 | } 77 | local url = string.format("https://bgmlist.com/api/v1/bangumi/archive/%s%s", year, q_tb[month]) 78 | local err, reply = kiko.httpget(url) 79 | if err ~= nil then error(err) end 80 | local content = reply["content"] 81 | local err, obj = kiko.json2table(content) 82 | if err ~= nil then 83 | error(err) 84 | end 85 | local bgmlist = {} 86 | for _, bgm in pairs(obj["items"]) do 87 | local title = bgm["title"] 88 | if bgm["titleTranslate"] ~= nil then 89 | local tt = bgm["titleTranslate"] 90 | if tt["zh-Hans"] ~= nil then 91 | title = tt["zh-Hans"][1] 92 | end 93 | end 94 | local airTimestamp = timestamp(bgm["begin"]) + 8*3600 95 | local airTime = os.date("*t", airTimestamp) 96 | local weekDay = airTime["wday"] 97 | weekDay = weekDay - 1 -- 0~6 98 | local bgmid = "" 99 | local sites = {} 100 | if bgm["sites"] ~= nil then 101 | for _, site in pairs(bgm["sites"]) do 102 | local site_name = site["site"] 103 | if site_name == nil then 104 | goto continue 105 | end 106 | if site_name=="bangumi" then 107 | bgmid=site["id"] 108 | goto continue 109 | else 110 | local siteinfo = site_map[site_name] 111 | if siteinfo ~= nil then 112 | local template = siteinfo["urlTemplate"] 113 | if site["id"]~=nil then 114 | local url = string.gsub(template, "{{id}}", site["id"]) 115 | table.insert(sites, { 116 | ["name"]=siteinfo["title"], 117 | ["url"]=url 118 | }) 119 | end 120 | end 121 | end 122 | ::continue:: 123 | end 124 | end 125 | local endTimestamp = -1 126 | if bgm["end"]~=nil and #bgm["end"]>0 then 127 | endTimestamp = timestamp(bgm["end"]) + 8*3600 128 | end 129 | table.insert(bgmlist, { 130 | ["title"]=title, 131 | ["weekday"]=weekDay, 132 | ["time"]=string.format("%02d:%02d", airTime["hour"], airTime["min"]), 133 | ["date"]=string.format("%d-%02d-%02d", airTime["year"], airTime["month"], airTime["day"]), 134 | ["isnew"]=isnew(year, month, airTimestamp), 135 | ["bgmid"]=bgmid, 136 | ["sites"]=sites, 137 | ["end"]=endTimestamp 138 | }) 139 | end 140 | if settings["load_last_season"]=='y' then 141 | local last_month_tb = { 142 | ["01"]="10", 143 | ["04"]="01", 144 | ["07"]="04", 145 | ["10"]="07" 146 | } 147 | local last_year = tonumber(year) 148 | if month=="01" then 149 | last_year = last_year-1 150 | end 151 | local last_season = { 152 | ["title"]=string.format("%d-%s", last_year, last_month_tb[month]) 153 | } 154 | kiko.log("load_last_season", last_season["title"]) 155 | settings["load_last_season"]='n' 156 | local states, last_bgms = pcall(getbgmlist, last_season) 157 | if states then 158 | for _, l_bgm in pairs(last_bgms) do 159 | if l_bgm["end"]==-1 or isnew(year, month, l_bgm["end"]) then 160 | l_bgm["isnew"]=false 161 | table.insert(bgmlist, l_bgm) 162 | end 163 | end 164 | else 165 | kiko.log(string.format("get last season %s failed: ", last_season["title"]), last_bgms) 166 | end 167 | settings["load_last_season"]='y' 168 | end 169 | return bgmlist 170 | end -------------------------------------------------------------------------------- /danmu/youku.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "优酷", 3 | ["id"] = "Kikyou.d.youku", 4 | ["desc"] = "优酷弹幕脚本", 5 | ["version"] = "0.2", 6 | ["min_kiko"] = "2.0.0", 7 | ["label_color"] = "0x2683FF", 8 | } 9 | 10 | supportedURLsRe = { 11 | "(https?://)?v.youku.com/v_show/id_(.+)\\.html\\??(.*)?" 12 | } 13 | 14 | sampleSupporedURLs = { 15 | "https://v.youku.com/v_show/id_XNTkxMjQxNzM4MA==.html" 16 | } 17 | 18 | settings = { 19 | ["skip_ad_time"]={ 20 | ["title"]="跳过广告时长(s)", 21 | ["desc"]="跳过广告时长(s),弹幕播放时间会减去这个时长", 22 | ["default"]="0" 23 | }, 24 | ["user_agent"]={ 25 | ["title"]="UA", 26 | ["desc"]="User Agent", 27 | ["default"]="Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/108.0" 28 | } 29 | } 30 | 31 | function get_ts(date_str) 32 | --date_str = "2022-10-17 18:23:26" 33 | local year, month, day, hour, min, sec = date_str:match("(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)") 34 | local timestamp = os.time({ 35 | year = tonumber(year), 36 | month = tonumber(month), 37 | day = tonumber(day), 38 | hour = tonumber(hour), 39 | min = tonumber(min), 40 | sec = tonumber(sec), 41 | }) 42 | return timestamp 43 | end 44 | 45 | function escape(str) 46 | str = string.gsub (str, "([^0-9a-zA-Z !'()*._~-])", -- locale independent 47 | function (c) return string.format ("%%%02X", string.byte(c)) end) 48 | str = string.gsub (str, " ", "+") 49 | return str 50 | end 51 | 52 | function get_cookies(cookie_str) 53 | local cookies = {} 54 | if cookie_str == nil then return cookies end 55 | local cookie_rows = string.split(cookie_str, '\n') 56 | for _, cookie_row in ipairs(cookie_rows) do 57 | local cookie_items = string.split(cookie_row, ';') 58 | if #cookie_items > 0 then 59 | local kvs = string.split(cookie_items[1], '=') 60 | if #kvs > 1 then 61 | cookies[kvs[1]] = kvs[2] 62 | end 63 | end 64 | end 65 | return cookies 66 | end 67 | 68 | function get_video_info(vid) 69 | local info_url = string.format("https://openapi.youku.com/v2/videos/show.json?client_id=53e6cc67237fc59a&package=com.huawei.hwvplayer.youku&ext=show&video_id=%s", vid) 70 | local err, reply = kiko.httpget(info_url, {}, {["User-Agent"] = settings["user_agent"]}) 71 | if err ~= nil then error(err) end 72 | local err, obj = kiko.json2table(reply["content"]) 73 | if err ~= nil then error(err) end 74 | if obj["duration"] == nil then error("未找到视频时长") end 75 | local duration = tonumber(obj["duration"]) 76 | local title = obj["title"] 77 | if title == nil then title = "unknown" end 78 | return title, duration 79 | end 80 | 81 | function download_seg(cna, vid, seg, danmuList) 82 | local msg = { 83 | ["ctime"] = tostring(os.time() * 1000), 84 | ["ctype"] = 10004, 85 | ["cver"] = "v1.0", 86 | ["guid"] = cna, 87 | ["mat"] = seg, 88 | ["mcount"] = 1, 89 | ["pid"] = 0, 90 | ["sver"] = "3.1.0", 91 | ["type"] = 1, 92 | ["vid"] = vid 93 | } 94 | local err, msg_json = kiko.table2json(msg, "compact") 95 | local err, msg_b64 = kiko.base64(msg_json, "to") 96 | msg["msg"] = msg_b64 97 | local err, sign = kiko.hashdata(msg_b64 .. "MkmC9SoIw6xCkSKHhJ7b5D2r51kBiREr", false) 98 | msg["sign"] = sign 99 | local err, reply = kiko.httpget("https://acs.youku.com/h5/mtop.com.youku.aplatform.weakget/1.0/?jsv=2.5.1&appKey=24679788", {}, {["User-Agent"] = settings["user_agent"]}) 100 | if err ~= nil then 101 | kiko.log(string.format("vid: %s, seg: %d, http error: %s", vid, seg, err)) 102 | return danmuList 103 | end 104 | local cookies = get_cookies(reply["headers"]["Set-Cookie"]) 105 | if cookies["_m_h5_tk_enc"] == nil or cookies["_m_h5_tk"] == nil then 106 | kiko.log(string.format("vid: %s, seg: %d, error: Cookie not found, %s", vid, seg, err)) 107 | return danmuList 108 | end 109 | local cookie_header = "" 110 | for k, v in pairs(cookies) do 111 | local c = string.format("%s=%s", k, v) 112 | if #cookie_header == 0 then 113 | cookie_header = c 114 | else 115 | cookie_header = cookie_header .. ";" .. c 116 | end 117 | end 118 | local headers = { 119 | ["Content-Type"] = "application/x-www-form-urlencoded", 120 | ["Cookie"] = cookie_header, 121 | ["Referer"] = "https://v.youku.com", 122 | ["User-Agent"] = settings["user_agent"] 123 | } 124 | local err, data = kiko.table2json(msg, "compact") 125 | local t = tostring(os.time() * 1000) 126 | local t_sing_hash_data = string.sub(cookies["_m_h5_tk"], 1, 32) .. "&" .. t .. "&" .. "24679788" .. "&" .. data 127 | local err, t_sign = kiko.hashdata(t_sing_hash_data, false) 128 | local params = { 129 | ["jsv"] = "2.5.6", 130 | ["appKey"] = "24679788", 131 | ["t"] = t, 132 | ["sign"] = t_sign, 133 | ["api"] = "mopen.youku.danmu.list", 134 | ["v"] = "1.0", 135 | ["type"] = "originaljson", 136 | ["dataType"] = "jsonp", 137 | ["timeout"] = "20000", 138 | ["jsonpIncPrefix"] = "utility" 139 | } 140 | local err, reply = kiko.httppost("https://acs.youku.com/h5/mopen.youku.danmu.list/1.0/", "data=" .. escape(data), headers, params) 141 | if err ~= nil then 142 | kiko.log(string.format("vid: %s, seg: %d, error: %s", vid, seg, err)) 143 | return danmuList 144 | end 145 | local err, obj = kiko.json2table(reply["content"]) 146 | if err ~= nil then 147 | kiko.log(string.format("dm content error, vid: %s, seg: %d, error: %s", vid, seg, err)) 148 | return danmuList 149 | end 150 | local err, dobj = kiko.json2table(obj["data"]["result"]) 151 | if err ~= nil then 152 | kiko.log(string.format("dmobj error, vid: %s, seg: %d, error: %s", vid, seg, err)) 153 | return danmuList 154 | end 155 | local skip_time = tonumber(settings["skip_ad_time"]) * 1000 156 | for _, dm in ipairs(dobj["data"]["result"]) do 157 | local color = 0xffffff 158 | local size = 0 159 | local err, pobj = kiko.json2table(dm["propertis"]) 160 | if err==nil then 161 | if pobj["color"] ~= nil then 162 | color = tonumber(pobj["color"]) 163 | end 164 | if pobj["size"] ~= nil and tonumber(pobj["size"]) > 2 then 165 | size = 2 166 | end 167 | end 168 | table.insert(danmuList, { 169 | ["text"]=dm["content"], 170 | ["time"]=dm["playat"]-skip_time, 171 | ["date"]=tostring(get_ts(dm["createtime"])), 172 | ["color"]=color, 173 | ["fontsize"]=size, 174 | ["sender"]="[Youku]" .. dm["uid2"] 175 | }) 176 | end 177 | return danmuList 178 | end 179 | 180 | function urlinfo(url) 181 | local reg = kiko.regex("[\\s\\S]+?youku.com/v_show/id_(.+?)\\.html") 182 | local _, _, vid = reg:find(url) 183 | 184 | if vid == nil then error("不支持的URL") end 185 | local title, duration = get_video_info(vid) 186 | if title == nil then title = "unknown" end 187 | 188 | local data = { ["vid"] = vid, ["duration"] = duration } 189 | local _, data_str = kiko.table2json(data, "compact") 190 | 191 | local results = {} 192 | table.insert(results, { 193 | ["title"] = title, 194 | ["duration"] = duration, 195 | ["data"] = data_str 196 | }) 197 | return results 198 | end 199 | 200 | function danmu(source) 201 | local err, source_obj = kiko.json2table(source["data"]) 202 | if err ~= nil then error(err) end 203 | 204 | local err, reply = kiko.httpget("https://log.mmstat.com/eg.js", {}, {["User-Agent"] = settings["user_agent"], ["Accept-Encoding"] = "gzip, deflate", ["Accept"] = "*/*", ["Connection"] = "keep-alive"}) 205 | local cookie_header = reply["headers"]["Set-Cookie"] or reply["headers"]["set-cookie"] 206 | local cookies = get_cookies(cookie_header) 207 | if cookies["cna"] == nil then error("get cna cookie failed") end 208 | 209 | local segs = math.floor(source_obj["duration"] / 60) + 1 210 | local danmuList = {} 211 | for i = 1,segs do 212 | download_seg(cookies["cna"], source_obj["vid"], i, danmuList) 213 | end 214 | return nil, danmuList 215 | end 216 | -------------------------------------------------------------------------------- /danmu/bahamut.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "动画疯", 3 | ["id"] = "Kikyou.d.Gamer", 4 | ["desc"] = "巴哈姆特动画疯弹幕脚本", 5 | ["version"] = "0.2", 6 | ["min_kiko"] = "2.0.0", 7 | ["label_color"] = "0x4798AA", 8 | } 9 | 10 | supportedURLsRe = { 11 | "(https?://)?ani\\.gamer\\.com\\.tw/animeVideo\\.php\\?sn=[0-9]+/?" 12 | } 13 | 14 | sampleSupporedURLs = { 15 | "https://ani.gamer.com.tw/animeVideo.php?sn=9285" 16 | } 17 | 18 | scriptmenus = { 19 | {["title"]="打开动画疯网站", ["id"]="open_gamer"}, 20 | {["title"]="登录", ["id"]="gamer_login"}, 21 | } 22 | 23 | cur_dm_cookie = '' 24 | 25 | function scriptmenuclick(menuid) 26 | if menuid == "open_gamer" then 27 | kiko.execute(true, "cmd", {"/c", "start", "https://ani.gamer.com.tw/"}) 28 | elseif menuid == "gamer_login" then 29 | local b = kiko.browser.create() 30 | local succ = b:load("https://user.gamer.com.tw/login.php") 31 | b:show("登录成功后关闭页面") 32 | cur_dm_cookie = b:runjs("document.cookie") 33 | kiko.log(cur_dm_cookie) 34 | end 35 | end 36 | 37 | function search(keyword) 38 | local _, tradKw = kiko.sttrans(keyword, false) 39 | local query = { 40 | ["keyword"]=tradKw 41 | } 42 | 43 | local cookies = kiko.browser.cookie(".gamer.com.tw") 44 | local cookie_str = "" 45 | for k, v in pairs(cookies) do 46 | cookie_str = cookie_str .. string.format("%s=%s; ", k, v) 47 | end 48 | 49 | local browser_search = function() 50 | local b = kiko.browser.create() 51 | local succ = b:load("https://ani.gamer.com.tw/search.php", query, 20000) 52 | local content = b:html() 53 | local _, _, searchContent = string.find(content, "
    (.+)
    ") 54 | if searchContent == nil then 55 | _, _, searchContent = string.find(content, "
    (.+)
    ") 56 | end 57 | 58 | if searchContent == nil then -- 手动验证 59 | b:show("验证成功跳转后关闭页面") 60 | content = b:html() 61 | end 62 | return content 63 | end 64 | 65 | local content = '' 66 | if #cookie_str == 0 then 67 | content = browser_search() 68 | else 69 | local headers = { 70 | ["User-Agent"] = kiko.browser.ua(), 71 | ["Cookie"] = cookie_str, 72 | } 73 | local err, reply = kiko.httpget("https://ani.gamer.com.tw/search.php", query, headers) 74 | if err ~= nil then 75 | content = browser_search() 76 | else 77 | content = reply["content"] 78 | end 79 | end 80 | 81 | local _, _, searchContent = string.find(content, "
    (.+)
    ") 82 | if searchContent == nil then 83 | _, _, searchContent = string.find(content, "
    (.+)
    ") 84 | end 85 | if searchContent == nil then return {} end 86 | 87 | local parser = kiko.htmlparser(searchContent) 88 | local curData, curTitle, curDesc = nil, nil, nil 89 | local results = {} 90 | while not parser:atend() do 91 | if parser:curproperty("class")=="theme-list-main" and parser:start() then 92 | local href = parser:curproperty("href") 93 | local _, _, sn = string.find(href, "animeRef%.php%?sn=(%d+)") 94 | if sn ~= nil then curData = sn end 95 | elseif parser:curproperty("class")=="theme-name" then 96 | curTitle = parser:readcontent() 97 | elseif parser:curproperty("class")=="theme-time" then 98 | curDesc = parser:readcontent() 99 | if curData ~= nil and curTitle ~= nil and curDesc ~= nil then 100 | local data = { 101 | ["sn"] = tostring(curData), 102 | ["fromSearch"] = true 103 | } 104 | local _, data_str = kiko.table2json(data) 105 | table.insert(results, { 106 | ["title"] = curTitle, 107 | ["desc"] = curDesc, 108 | ["data"] = data_str 109 | }) 110 | curData, curTitle, curDesc = nil, nil, nil 111 | end 112 | end 113 | parser:readnext() 114 | end 115 | return results 116 | end 117 | 118 | function epinfo(source) 119 | local err, source_obj = kiko.json2table(source["data"]) 120 | if err ~= nil then error(err) end 121 | local baseUrl = "https://ani.gamer.com.tw/animeVideo.php" 122 | if source_obj["fromSearch"] then 123 | baseUrl = "https://ani.gamer.com.tw/animeRef.php" 124 | end 125 | local query = { 126 | ["sn"]=source_obj["sn"] 127 | } 128 | 129 | local cookies = kiko.browser.cookie(".gamer.com.tw") 130 | local cookie_str = "" 131 | for k, v in pairs(cookies) do 132 | cookie_str = cookie_str .. string.format("%s=%s; ", k, v) 133 | end 134 | 135 | local browser_search = function() 136 | local b = kiko.browser.create() 137 | local succ = b:load(baseUrl, query, 20000) 138 | local content = b:html() 139 | local _, _, epContent = string.find(content, "
    (.-)
    ") 140 | 141 | if epContent == nil then -- 手动验证 142 | b:show("验证成功跳转后关闭页面") 143 | content = b:html() 144 | end 145 | return content 146 | end 147 | 148 | local content = '' 149 | if #cookie_str == 0 then 150 | content = browser_search() 151 | else 152 | local headers = { 153 | ["User-Agent"] = kiko.browser.ua(), 154 | ["Cookie"] = cookie_str, 155 | } 156 | local err, reply = kiko.httpget(baseUrl, query, headers) 157 | if err ~= nil then 158 | content = browser_search() 159 | else 160 | content = reply["content"] 161 | end 162 | end 163 | 164 | local _, _, epContent = string.find(content, "
    (.-)
    ") 165 | local results = {} 166 | if epContent ~= nil then 167 | local parser = kiko.htmlparser(epContent) 168 | while not parser:atend() do 169 | if parser:curnode()=="a" and parser:start() then 170 | local href = parser:curproperty("href") 171 | local _, _, sn = string.find(href, "%?sn=(%d+)") 172 | if sn ~= nil then 173 | local data = { 174 | ["sn"] = sn 175 | } 176 | local _, data_str = kiko.table2json(data) 177 | table.insert(results, { 178 | ["title"] = parser:readcontent(), 179 | ["data"] = data_str 180 | }) 181 | end 182 | end 183 | parser:readnext() 184 | end 185 | else 186 | local _, _, sn, title = string.find(content, "animefun.videoSn.?=.?(%d+);.-animefun.title.?=.?'(.-)'") 187 | if sn ~= nil and title ~= nil then 188 | local data = { 189 | ["sn"] = sn 190 | } 191 | local _, data_str = kiko.table2json(data) 192 | table.insert(results, { 193 | ["title"] = title, 194 | ["data"] = data_str 195 | }) 196 | end 197 | end 198 | return results 199 | end 200 | 201 | function urlinfo(url) 202 | local pattens = { 203 | ["https?://ani%.gamer%.com%.tw/animeVideo%.php%?sn=%d+"]="sn", 204 | ["ani%.gamer%.com%.tw/animeVideo%.php%?sn=%d+"]="sn" 205 | } 206 | local matched = nil 207 | for pv, k in pairs(pattens) do 208 | s, e = string.find(url, pv) 209 | if s then 210 | if e - s + 1 == #url then 211 | matched = k 212 | break 213 | end 214 | end 215 | end 216 | if matched == nil then error("不支持的URL") end 217 | local _, _, sn = string.find(url, "sn=(%d+)") 218 | local data = { 219 | ["sn"] = sn 220 | } 221 | local _, data_str = kiko.table2json(data) 222 | return epinfo({ 223 | ["data"] = data_str 224 | }) 225 | end 226 | 227 | function danmu(source) 228 | local err, source_obj = kiko.json2table(source["data"]) 229 | if err ~= nil then error(err) end 230 | 231 | local danmuUrl = "https://api.gamer.com.tw/anime/v1/danmu.php" 232 | local query = { 233 | ["videoSn"]=source_obj["sn"], 234 | ["geo"]="TW,HK", 235 | } 236 | 237 | local headers = { 238 | ["User-Agent"] = kiko.browser.ua(), 239 | } 240 | 241 | if #cur_dm_cookie > 0 then 242 | headers["Cookie"] = cur_dm_cookie 243 | else 244 | local cookies = kiko.browser.cookie(".gamer.com.tw") 245 | local cookie_str = "" 246 | for k, v in pairs(cookies) do 247 | cookie_str = cookie_str .. string.format("%s=%s; ", k, v) 248 | end 249 | headers["Cookie"] = cookie_str 250 | end 251 | 252 | local err, reply = kiko.httpget(danmuUrl, query, headers) 253 | if err ~= nil then error(err) end 254 | local err, obj = kiko.json2table(reply["content"]) 255 | local array = obj["data"]["danmu"] 256 | if err ~= nil or array == nil then error(err) end 257 | 258 | local danmus = {} 259 | for _, dmObj in ipairs(array) do 260 | local text = dmObj["text"] 261 | local time = tonumber(dmObj["time"])*100 262 | local pos = tonumber(dmObj["position"]) 263 | local dmType = pos -- pos=1(top),pos=2(bottom) 264 | local size = tonumber(dmObj["size"]) 265 | if size == 0 then --small 266 | size = 1 267 | elseif size == 2 then --large 268 | size = 2 269 | else --normal 270 | size = 0 271 | end 272 | local color = tonumber(string.sub(dmObj["color"], 2),16) 273 | local sender = "[Gamer]" .. tostring(dmObj["userid"]) 274 | table.insert(danmus, { 275 | ["text"]=text, 276 | ["time"]=time, 277 | ["color"]=color, 278 | ["fontsize"]=size, 279 | ["type"]=dmType, 280 | ["sender"]=sender 281 | }) 282 | end 283 | return nil, danmus 284 | end 285 | -------------------------------------------------------------------------------- /danmu/bilibili.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "Bilibili", 3 | ["id"] = "Kikyou.d.Bilibili", 4 | ["desc"] = "Bilibili弹幕脚本", 5 | ["version"] = "0.3.2", 6 | ["min_kiko"] = "2.0.0", 7 | ["label_color"] = "0xDC478A", 8 | } 9 | 10 | supportedURLsRe = { 11 | "(https?://)?www\\.bilibili\\.com/video/av[0-9]+/?", 12 | "(https?://)?www\\.bilibili\\.com/video/BV[\\dA-Za-z]+/?", 13 | "av[0-9]+", 14 | "BV[\\dA-Za-z]+", 15 | "(https?://)?www\\.bilibili\\.com/bangumi/media/md[0-9]+/?", 16 | "(https?://)?www\\.bilibili\\.com/bangumi/play/ss[0-9]+/?", 17 | "(https?://)?www\\.bilibili\\.com/bangumi/play/ep[0-9]+/?" 18 | } 19 | 20 | sampleSupporedURLs = { 21 | "https://www.bilibili.com/video/av1728704", 22 | "https://www.bilibili.com/video/BV11x411P7TB", 23 | "av24213033", 24 | "BV11x411P7TB", 25 | "https://www.bilibili.com/bangumi/media/md28221404", 26 | "https://www.bilibili.com/bangumi/play/ss36429", 27 | "https://www.bilibili.com/bangumi/play/ep409605" 28 | } 29 | 30 | function str2time(time_str) 31 | local pos = 0 32 | for i = 1,#time_str do 33 | local ch = string.sub(time_str, i, i) 34 | if ch == ':' then 35 | pos = i 36 | break 37 | end 38 | end 39 | if pos == 0 then 40 | return tonumber(time_str) or 0 41 | else 42 | local minute = tonumber(string.sub(time_str, 1, pos-1)) or 0 43 | local second = tonumber(string.sub(time_str, pos+1)) or 0 44 | return minute*60+second 45 | end 46 | end 47 | 48 | function search(keyword) 49 | local query = { 50 | ["keyword"]=keyword 51 | } 52 | local header = { 53 | ["Accept"]="application/json" 54 | } 55 | local err, reply = kiko.httpget("https://api.bilibili.com/x/web-interface/search/all", query, header) 56 | if err ~= nil then error(err) end 57 | local content = reply["content"] 58 | local err, obj = kiko.json2table(content) 59 | if err ~= nil then 60 | error(err) 61 | end 62 | local ret = obj["data"]["result"] 63 | if ret == nil then 64 | return {} 65 | end 66 | local bangumiResult, ftResult, videoResult = ret["media_bangumi"], ret["media_ft"], ret["video"] 67 | local results = {} 68 | if bangumiResult and ftResult then 69 | local bgmResult = table.move(ftResult, 1, #ftResult, #bangumiResult+1, bangumiResult) 70 | for _, sobj in ipairs(bgmResult) do 71 | local data = { 72 | ["media_id"] = string.format("%d", sobj["media_id"]), 73 | ["season_id"] = string.format("%d", sobj["season_id"]), 74 | ["stype"] = "collection" 75 | } 76 | local _, data_str = kiko.table2json(data) 77 | table.insert(results, { 78 | ["title"] = string.gsub(sobj["title"], "<([^<>]*)em([^<>]*)>", ""), 79 | ["desc"] = sobj["desc"], 80 | ["data"] = data_str 81 | }) 82 | end 83 | end 84 | if videoResult then 85 | for _, sobj in ipairs(videoResult) do 86 | local data = { 87 | ["aid"] = string.format("%d", sobj["id"]), 88 | ["bvid"] = sobj["bvid"], 89 | ["stype"] = "video" 90 | } 91 | local _, data_str = kiko.table2json(data) 92 | table.insert(results, { 93 | ["title"] = string.gsub(sobj["title"], "<([^<>]*)em([^<>]*)>", ""), 94 | ["desc"] = sobj["description"], 95 | ["duration"] = str2time(sobj["duration"]), 96 | ["data"] = data_str 97 | }) 98 | end 99 | end 100 | return results 101 | end 102 | 103 | function epinfo(source) 104 | local err, source_obj = kiko.json2table(source["data"]) 105 | if err ~= nil then error(err) end 106 | if source_obj["stype"]=="collection" or source_obj["stype"]=="collection_ss" then 107 | local query = { ["season_id"] = source_obj["season_id"] } 108 | local header = { ["Accept"]="application/json" } 109 | local err, reply = kiko.httpget("https://api.bilibili.com/pgc/view/web/season", query, header) 110 | if err ~= nil then error(err) end 111 | local content = reply["content"] 112 | local err, obj = kiko.json2table(content) 113 | if err ~= nil then error(err) end 114 | local results = {} 115 | for _, bobj in ipairs(obj["result"]["episodes"]) do 116 | local index = bobj["title"] 117 | local title = bobj["long_title"] 118 | local data = { 119 | ["aid"] = string.format("%d", bobj["aid"]), 120 | ["cid"] = string.format("%d", bobj["cid"]), 121 | ["bvid"] = bobj["bvid"], 122 | ["stype"] = "video" 123 | } 124 | local _, data_str = kiko.table2json(data) 125 | local fTitle = string.format("%s-%s", index, title) 126 | if title == nil or #title == 0 then 127 | fTitle = index 128 | end 129 | table.insert(results, { 130 | ["title"] = fTitle, 131 | ["duration"] = bobj["duration"]/1000, 132 | ["data"] = data_str 133 | }) 134 | end 135 | return results 136 | elseif source_obj["stype"]=="collection_ep" then 137 | local query = { ["ep_id"] = source_obj["ep_id"] } 138 | local header = { ["Accept"]="application/json" } 139 | local err, reply = kiko.httpget("https://api.bilibili.com/pgc/view/web/season", query, header) 140 | if err ~= nil then error(err) end 141 | local content = reply["content"] 142 | local err, obj = kiko.json2table(content) 143 | if err ~= nil then error(err) end 144 | local results = {} 145 | for _, bobj in ipairs(obj["result"]["episodes"]) do 146 | local index = bobj["title"] 147 | local title = bobj["long_title"] 148 | local data = { 149 | ["aid"] = string.format("%d", bobj["aid"]), 150 | ["cid"] = string.format("%d", bobj["cid"]), 151 | ["bvid"] = bobj["bvid"], 152 | ["stype"] = "video" 153 | } 154 | local _, data_str = kiko.table2json(data) 155 | local fTitle = string.format("%s-%s", index, title) 156 | if title == nil or #title == 0 then 157 | fTitle = index 158 | end 159 | table.insert(results, { 160 | ["title"] = fTitle, 161 | ["duration"] = bobj["duration"]/1000, 162 | ["data"] = data_str 163 | }) 164 | end 165 | return results 166 | elseif source_obj["stype"]=="video" then 167 | local query = {} 168 | if source_obj["aid"] ~= nil then 169 | query["aid"] = source_obj["aid"] 170 | else 171 | query["bvid"] = source_obj["bvid"] 172 | end 173 | local header = { ["Accept"]="application/json" } 174 | local err, reply = kiko.httpget("http://api.bilibili.com/x/web-interface/view", query, header) 175 | if err ~= nil then error(err) end 176 | local content = reply["content"] 177 | local err, obj = kiko.json2table(content) 178 | if err ~= nil then error(err) end 179 | local obj = obj["data"] 180 | local aid = string.format("%d", obj["aid"]) 181 | local results = {} 182 | for i, bobj in ipairs(obj["pages"]) do 183 | local data = { 184 | ["cid"] = string.format("%d", bobj["cid"]), 185 | ["stype"] = "video" 186 | } 187 | if i == 1 then 188 | data["aid"] = aid 189 | data["bvid"] = obj["bvid"] 190 | end 191 | local _, data_str = kiko.table2json(data) 192 | table.insert(results, { 193 | ["title"] = bobj["part"], 194 | ["duration"] = bobj["duration"] or 0, 195 | ["data"] = data_str 196 | }) 197 | end 198 | return results 199 | else 200 | return {} 201 | end 202 | end 203 | 204 | function urlinfo(url) 205 | local pattens = { 206 | ["https?://www%.bilibili%.com/video/av[0-9]+/?"]="av", 207 | ["www%.bilibili%.com/video/av[0-9]+/?"]="av", 208 | ["https?://www%.bilibili%.com/video/BV[%dA-Za-z]+/?"]="bv", 209 | ["www%.bilibili%.com/video/BV[%dA-Za-z]+/?"]="bv", 210 | ["av%d+"]="av", 211 | ["BV[%dA-Za-z]+"]="bv", 212 | ["https?://www%.bilibili%.com/bangumi/media/md[0-9]+/?"]="bgm", 213 | ["www%.bilibili%.com/bangumi/media/md[0-9]+/?"]="bgm", 214 | ["https?://www%.bilibili%.com/bangumi/play/ss[0-9]+/?"]="bgm_play_ss", 215 | ["www%.bilibili%.com/bangumi/play/ss[0-9]+/?"]="bgm_play_ss", 216 | ["https?://www%.bilibili%.com/bangumi/play/ep[0-9]+/?"]="bgm_play_ep", 217 | ["www%.bilibili%.com/bangumi/play/ep[0-9]+/?"]="bgm_play_ep" 218 | } 219 | local matched = nil 220 | for pv, k in pairs(pattens) do 221 | s, e = string.find(url, pv) 222 | if s then 223 | if e - s + 1 == #url then 224 | matched = k 225 | break 226 | end 227 | end 228 | end 229 | if matched == nil then error("不支持的URL") end 230 | if matched == "av" then 231 | local _, _, aid = string.find(url, "av(%d+)") 232 | return epinfo({ 233 | ["data"] = string.format("{\"aid\":\"%s\", \"stype\": \"video\"}", aid) 234 | }) 235 | elseif matched == "bgm" then 236 | local _, _, bgmid = string.find(url, "md(%d+)") 237 | local query = { ["media_id"] = bgmid } 238 | local header = { ["Accept"]="application/json" } 239 | local err, reply = kiko.httpget("https://api.bilibili.com/pgc/review/user", query, header) 240 | if err ~= nil then error(err) end 241 | local content = reply["content"] 242 | local err, obj = kiko.json2table(content) 243 | if err ~= nil then error(err) end 244 | local obj = obj["result"]["media"] 245 | local season_id = obj["season_id"] 246 | if season_id == nil then error("invalid media_id") end 247 | return epinfo({ 248 | ["data"] = string.format("{\"season_id\":\"%s\", \"stype\": \"collection\"}", season_id) 249 | }) 250 | elseif matched == "bgm_play_ss" then 251 | local _, _, bgmid = string.find(url, "ss(%d+)") 252 | return epinfo({ 253 | ["data"] = string.format("{\"season_id\":\"%s\", \"stype\": \"collection_ss\"}", bgmid) 254 | }) 255 | elseif matched == "bgm_play_ep" then 256 | local _, _, bgmid = string.find(url, "ep(%d+)") 257 | return epinfo({ 258 | ["data"] = string.format("{\"ep_id\":\"%s\", \"stype\": \"collection_ep\"}", bgmid) 259 | }) 260 | elseif matched == "bv" then 261 | local _, _, bvid = string.find(url, "(BV[%dA-Za-z]+)") 262 | return epinfo({ 263 | ["data"] = string.format("{\"bvid\":\"%s\", \"stype\": \"video\"}", bvid) 264 | }) 265 | end 266 | end 267 | 268 | function danmu(source) 269 | local err, source_obj = kiko.json2table(source["data"]) 270 | if err ~= nil then error(err) end 271 | local cid = source_obj["cid"] 272 | if cid == nil then return {} end 273 | local err, reply = kiko.httpget(string.format("http://comment.bilibili.com/%s.xml", cid)) 274 | if err ~= nil then error(err) end 275 | local content = reply["content"] 276 | local danmus = {} 277 | local xmlreader = kiko.xmlreader(content) 278 | while not xmlreader:atend() do 279 | if xmlreader:startelem() and xmlreader:name()=="d" then 280 | if xmlreader:hasattr("p") then 281 | local attrs = string.split(xmlreader:attr("p"), ',') 282 | if #attrs >= 8 then 283 | local text = xmlreader:elemtext() 284 | local time = tonumber(attrs[1]) * 1000 285 | local mode = tonumber(attrs[2]) 286 | local dmType = 0 --rolling 287 | if mode == 4 then 288 | dmType = 2 --bottom 289 | elseif mode == 5 then 290 | dmType = 1 --top 291 | end 292 | local size = tonumber(attrs[3]) 293 | if size == 18 then 294 | size = 1 295 | elseif size == 36 then 296 | size = 2 297 | else 298 | size = 0 299 | end 300 | local color = tonumber(attrs[4]) 301 | local date = attrs[5] 302 | local sender = "[Bilibili]" .. attrs[7] 303 | table.insert(danmus, { 304 | ["text"]=text, 305 | ["time"]=time, 306 | ["color"]=color, 307 | ["fontsize"]=size, 308 | ["type"]=dmType, 309 | ["date"]=date, 310 | ["sender"]=sender 311 | }) 312 | end 313 | end 314 | end 315 | xmlreader:readnext() 316 | end 317 | return nil, danmus 318 | end 319 | -------------------------------------------------------------------------------- /library/bangumi.lua: -------------------------------------------------------------------------------- 1 | info = { 2 | ["name"] = "Bangumi", 3 | ["id"] = "Kikyou.l.Bangumi", 4 | ["desc"] = "Bangumi脚本,从bgm.tv中获取动画信息", 5 | ["version"] = "0.3.1", 6 | ["min_kiko"] = "0.9.1" 7 | } 8 | 9 | settings = { 10 | ["cover_quality"]={ 11 | ["title"]="封面图质量", 12 | ["default"]="common", 13 | ["desc"]="图片质量从高到低:medium(中), common(正常), large(高)", 14 | ["choices"]="medium,common,large" 15 | }, 16 | ["tag_staff"]={ 17 | ["title"]="添加Staff标签", 18 | ["default"]="y", 19 | ["desc"]="搜索标签时是否显示Staff层次标签", 20 | ["choices"]="y,n" 21 | }, 22 | ["tag_actor"]={ 23 | ["title"]="添加演员标签", 24 | ["default"]="y", 25 | ["desc"]="搜索标签时是否显示演员层次标签", 26 | ["choices"]="y,n" 27 | } 28 | } 29 | 30 | searchsettings = { 31 | ["result_type"]={ 32 | ["title"]="搜索结果类型", 33 | ["default"]="动画", 34 | ["desc"]="搜索结果类型", 35 | ["choices"]="书籍,动画,音乐,游戏,三次元", 36 | ["display_type"] = 2 37 | }, 38 | } 39 | 40 | menus = { 41 | {["title"]="打开Bangumi页面", ["id"]="open_bgm"} 42 | } 43 | 44 | function menuclick(menuid, anime) 45 | local NM_HIDE=1 46 | local NM_PROCESS=2 47 | local NM_SHOWCANCEL = 4 48 | local NM_ERROR = 8 49 | local NM_DARKNESS_BACK = 16 50 | kiko.log("Menu Click: ", menuid) 51 | if menuid == "open_bgm" then 52 | kiko.message("Menu Action: Open BGM", NM_HIDE) 53 | kiko.execute(true, "cmd", {"/c", "start", anime["url"]}) 54 | end 55 | end 56 | 57 | function setoption(key, val) 58 | kiko.log(string.format("Setting changed: %s = %s", key, val)) 59 | end 60 | 61 | function search(keyword, options) 62 | local type_map = { 63 | ["书籍"] = 1, 64 | ["动画"] = 2, 65 | ["音乐"] = 3, 66 | ["游戏"] = 4, 67 | ["三次元"] = 6, 68 | } 69 | local query = { 70 | ["type"]=type_map[options["result_type"]], 71 | ["responseGroup"]="small", 72 | ["start"]="0", 73 | ["max_results"]="10" 74 | } 75 | local header = { 76 | ["Accept"]="application/json" 77 | } 78 | local err, reply = kiko.httpget(string.format("https://api.bgm.tv/search/subject/%s", keyword), query, header) 79 | if err ~= nil then error(err) end 80 | local content = reply["content"] 81 | local err, obj = kiko.json2table(content) 82 | if err ~= nil then 83 | error(err) 84 | end 85 | local animes = {} 86 | for _, anime in pairs(obj['list']) do 87 | local animeName = unescape(anime["name_cn"] or anime["name"]) 88 | if #animeName==0 then 89 | animeName = unescape(anime["name"]) 90 | end 91 | local data = string.format("%d", anime["id"]) 92 | local epList = {} 93 | table.insert(animes, { 94 | ["name"]=animeName, 95 | ["data"]=data, 96 | ["extra"]=data 97 | }) 98 | end 99 | return animes 100 | end 101 | 102 | function unescape(str) 103 | str = string.gsub( str, '<', '<' ) 104 | str = string.gsub( str, '>', '>' ) 105 | str = string.gsub( str, '"', '"' ) 106 | str = string.gsub( str, ''', "'" ) 107 | str = string.gsub( str, '&#(%d+);', function(n) return utf8.char(n) end ) 108 | str = string.gsub( str, '&#x(%x+);', function(n) return utf8.char(tonumber(n,16)) end ) 109 | str = string.gsub( str, '&', '&' ) -- Be sure to do this after all others 110 | return str 111 | end 112 | 113 | function getep(anime) 114 | local bgmId = anime["data"] 115 | local header = { 116 | ["Accept"]="application/json" 117 | } 118 | local err, reply = kiko.httpget(string.format("https://api.bgm.tv/subject/%s/ep", bgmId), {}, header) 119 | if err ~= nil then error(err) end 120 | local content = reply["content"] 121 | local err, obj = kiko.json2table(content) 122 | if err ~= nil then 123 | error(err) 124 | end 125 | local eps = {} 126 | for _, ep in pairs(obj['eps']) do 127 | local epType = ep["type"] + 1 -- ep["type"]: 0~6 128 | local epIndex = ep["sort"] 129 | local epName = unescape(ep["name_cn"] or ep["name"]) 130 | if #epName==0 then epName = unescape(ep["name"]) end 131 | table.insert(eps, { 132 | ["name"]=epName, 133 | ["index"]=epIndex, 134 | ["type"]=epType 135 | }) 136 | end 137 | return eps 138 | end 139 | 140 | function getStaff(staffArray) 141 | if staffArray == nil then 142 | return "" 143 | end 144 | local jobstrs = {} 145 | local invalidJos = Set({ 146 | "中文名", "别名", "话数", "放送开始", "放送星期", "IMDb", 147 | }) 148 | for _, staff in pairs(staffArray) do 149 | local job = staff["key"] 150 | if invalidJos[job] == nil and type(staff["value"]) == "string" then 151 | table.insert(jobstrs, job..":"..staff["value"]) 152 | end 153 | end 154 | return table.concat(jobstrs, ";") 155 | end 156 | 157 | function getCrt(bgmId) 158 | local query = { 159 | ["responseGroup"]="medium", 160 | } 161 | local header = { 162 | ["Accept"]="application/json" 163 | } 164 | local err, reply = kiko.httpget(string.format("https://api.bgm.tv/subject/%s", bgmId), query, header) 165 | if err ~= nil then 166 | kiko.log(err) 167 | return {} 168 | end 169 | local content = reply["content"] 170 | local err, obj = kiko.json2table(content) 171 | if err ~= nil then 172 | kiko.log(err) 173 | return {} 174 | end 175 | local crts = {} 176 | local crtArray = obj["crt"] 177 | if crtArray == nil then 178 | return crts 179 | end 180 | for _, crt in pairs(crtArray) do 181 | if crt["actors"] ~= nil then 182 | local _, actor = kiko.sttrans(crt["actors"][1]["name"], true) 183 | local img = crt["images"] 184 | local imgurl = "" 185 | if img ~= nil then imgurl = img["grid"] end 186 | local crt_name = unescape(crt["name_cn"] or crt["name"]) 187 | if #crt_name==0 then crt_name = unescape(crt["name"]) end 188 | table.insert(crts, { 189 | ["name"]=crt_name, 190 | ["actor"]=actor, 191 | ["link"]=string.format("http://bgm.tv/character/%d", crt["id"]), 192 | ["imgurl"]=imgurl 193 | }) 194 | end 195 | end 196 | return crts 197 | end 198 | 199 | function detail(anime) 200 | local bgmId = anime["data"] 201 | query = { 202 | ["responseGroup"]="medium", 203 | } 204 | local header = { 205 | ["Accept"]="application/json" 206 | } 207 | local err, reply = kiko.httpget(string.format("https://api.bgm.tv/v0/subjects/%s", bgmId), query, header) 208 | if err ~= nil then error(err) end 209 | local content = reply["content"] 210 | local err, obj = kiko.json2table(content) 211 | if err ~= nil then 212 | error(err) 213 | end 214 | local animeName = unescape(obj["name_cn"] or obj["name"]) 215 | if #animeName==0 then animeName = unescape(anime["name"]) end 216 | 217 | local anime = { 218 | ["name"]=animeName, 219 | ["data"]=bgmId, 220 | ["url"]=string.format("http://bgm.tv/subject/%s", bgmId), 221 | ["desc"]=obj["summary"], 222 | ["airdate"]=obj["date"], 223 | ["epcount"]=obj["eps"], 224 | ["coverurl"]=obj["images"][settings["cover_quality"]], 225 | ["staff"]=getStaff(obj["infobox"]), 226 | ["crt"]=getCrt(bgmId) 227 | } 228 | return anime 229 | end 230 | 231 | function Set(list) 232 | local set = {} 233 | for _, l in ipairs(list) do set[l] = true end 234 | return set 235 | end 236 | 237 | function removeWhen(src, cond, removes) 238 | if src[cond] then 239 | for _, v in ipairs(removes) do 240 | src[v] = nil 241 | end 242 | end 243 | end 244 | 245 | function string.split(str, sep) 246 | local pStart = 1 247 | local nSplitIndex = 1 248 | local nSplitArray = {} 249 | while true do 250 | local pEnd = string.find(str, sep, pStart) 251 | if pEnd == pStart then 252 | pStart = pEnd + string.len(sep) 253 | else 254 | if not pEnd then 255 | nSplitArray[nSplitIndex] = string.sub(str, pStart, string.len(str)) 256 | break 257 | end 258 | nSplitArray[nSplitIndex] = string.sub(str, pStart, pEnd - 1) 259 | pStart = pEnd + string.len(sep) 260 | nSplitIndex = nSplitIndex + 1 261 | end 262 | end 263 | return nSplitArray 264 | end 265 | 266 | function getNames(anime) 267 | local anames = {} 268 | for _, c in pairs(anime["crt"]) do 269 | table.insert(anames, c["name"]) 270 | table.insert(anames, c["actor"]) 271 | end 272 | for _, v in pairs(anime["staff"]) do 273 | for _, n in ipairs(string.split(v, " ")) do 274 | table.insert(anames, n) 275 | end 276 | end 277 | return anames 278 | end 279 | 280 | function addLevelTags(tags, anime) 281 | local studios = { 282 | ["BONES"] = "BONES", 283 | ["京都动画"] ="京都动画", ["京都アニメーション"]="京都动画", 284 | ["Madhouse"]="MADHouse", ["MADHouse"] = "MADHouse", 285 | ["A-1 Pictures"]="A-1 Pictures", ["A-1Pictures"]="A-1 Pictures", ["A-1_Pictures"]="A-1 Pictures", 286 | ["J.C.STAFF"]="J.C.STAFF", 287 | ["Feel."]="Feel.", 288 | ["Production I.G"]="Production I.G", ["Production.I.G"] = "Production I.G", ["ProductionI.G"] = "Production I.G", 289 | ["ufotable"]="ufotable", 290 | ["动画工房"]="动画工房", 291 | ["P.A.WORKS"]="P.A.WORKS", 292 | ["Studio Pierrot"]="Studio Pierrot", 293 | ["Studio DEEN"]="Studio DEEN", 294 | ["TOEI"]="TOEI", 295 | ["SUNRISE"]="SUNRISE", 296 | ["TRIGGER"]="TRIGGER", 297 | ["GAINAX"]="GAINAX", 298 | ["SHAFT"]="SHAFT", 299 | ["ZEXCS"]="ZEXCS", 300 | ["David Production"]="David Production", ["davidproduction"]="David Production", ["david_production"]="David Production", 301 | ["TROYCA"]="TROYCA", 302 | ["AIC"]="AIC", 303 | ["MAPPA"]="MAPPA", 304 | ["C2C"]="C2C", 305 | ["SILVERLINK."]="SILVERLINK.", ["SILVER_LINK."]="SILVERLINK.", ["SILVERLINK"]="SILVERLINK.", 306 | ["TMSEntertainment"]="TMS Entertainment", ["TMS"]="TMS Entertainment",["TMS_Entertainment"]="TMS Entertainment", 307 | ["NOMAD"] = "NOMAD", 308 | ["ZERO-G"]="ZERO-G", 309 | ["PINE JAM"]="PINE JAM", ["PINE_JAM"]="PINE JAM", ["PINEJAM"]="PINE JAM", 310 | ["8-Bit"]="8-Bit", ["8bit"]="8-Bit", 311 | ["Nexus"]="Nexus", 312 | ["Studio五组"]="Studio五组", 313 | ["project No.9"]="project No.9",["projectNo.9"]="project No.9", ["project_No.9"]="project No.9", 314 | ["手冢Production"]="手冢Production", ["手塚PRODUCTION"]="手冢Production", ["手塚プロダクション"]="手冢Production", 315 | ["CONNECT"]="CONNECT", 316 | ["LIDENFILMS"]="LIDENFILMS", 317 | ["Diomedéa"]="Diomedéa",["diomedéa"]="Diomedéa", ["diomedea"]="Diomedéa", 318 | ["GEEK TOYS"]="GeekToys", ["GeekToys"]="GeekToys", 319 | ["Lerche"]="Lerche", 320 | ["Passione"]="Passione", ["パッショーネ"]="Passione", 321 | ["Millepensee"]="Millepensee", ["millepensee"]="Millepensee", ["ミルパンセ"]="Millepensee", 322 | ["WHITE FOX"]="WHITE FOX", ["WHITEFOX"]="WHITE FOX", 323 | ["ENGI Inc."]="ENGI Inc.", ["Engi"]="ENGI Inc.", 324 | ["Hoods Entertainment"]="Hoods Entertainment", ["HoodsEntertainment"]="Hoods Entertainment", 325 | ["MAHO FILM"]="MAHO FILM", ["MAHOFILM"]="MAHO FILM", 326 | ["Lesprit"]="Lesprit", 327 | ["SANZIGEN Inc."]="SANZIGEN Inc.", ["SANZIGEN"]="SANZIGEN Inc.", 328 | ["Tear Studio"]="Tear Studio", ["Tear_Studio"]="Tear Studio", 329 | ["C-Station"]="C-Station", 330 | ["Seven Arcs"]="Seven Arcs", ["SEVEN_ARCS"]="Seven Arcs", ["SEVEN·ARCS"]="Seven Arcs", ["SEVEN・ARCS"]="Seven Arcs", ["SevenArcs"]="Seven Arcs", 331 | ["SHIN-EI"] = "新锐动画", ["SHIN-EI动画"] = "新锐动画", 332 | ["NAZ"] = "NAZ", 333 | ["旭production"] = "旭Production", 334 | ["KINEMACITRUS"] = "KINEMA CITRUS", 335 | ["StudioGaina"] = "Studio GAINA", 336 | ["StudioBind"] = "Studio Bind", 337 | ["BiburyAnimationStudios"] = "拜伯里动画工作室", 338 | ["st.SILVER"] = "st.SILVER", 339 | ["WITSTUDIO"] = "WIT STUDIO", 340 | ["StudioPalette"] = "Studio Palette", 341 | ["CloverWorks"] = "CloverWorks", 342 | } 343 | for i, tag in ipairs(tags) do 344 | if studios[tag] then 345 | tags[i] = "制作/"..studios[tag] 346 | end 347 | end 348 | local types = Set({ 349 | "轻改", "漫改", "游戏改", "原创" 350 | }) 351 | for i, tag in ipairs(tags) do 352 | if types[tag] then 353 | tags[i] = "改编类型/"..tags[i] 354 | end 355 | end 356 | if settings["tag_staff"]=='y' then 357 | for k, v in pairs(anime["staff"]) do 358 | if k=="导演" or k=="原作" then 359 | for _, n in ipairs(string.split(v, " ")) do 360 | table.insert(tags, k .. "/" .. n) 361 | end 362 | end 363 | end 364 | end 365 | if settings["tag_actor"]=='y' then 366 | local i = 0; 367 | for _, c in pairs(anime["crt"]) do 368 | table.insert(tags, "出演/" .. c["actor"]) 369 | i = i+1 370 | if i>5 then break end 371 | end 372 | end 373 | return tags 374 | end 375 | 376 | function tagFilter(tags, anime) 377 | local trivialTags = Set({ 378 | "TV", "OVA", "OAD", "WEB", "日本", "季番", "动画", "日本动画", "未确定", "追番", 379 | "佳作", "未上映", "未定档", "剧情", "TVA", 380 | "更多 +" 381 | }) 382 | local nameTags = Set(getNames(anime)) 383 | local containRemoveTags = {"OVA"} 384 | local fTags = {} 385 | local animeName = anime["name"] 386 | for _, tag in ipairs(tags) do 387 | repeat 388 | local _, tag = kiko.sttrans(tag, true) 389 | if trivialTags[tag] then break end 390 | if nameTags[tag] then break end 391 | if string.find(tag, "20%d%d") or string.find(tag, "19%d%d") then break end 392 | if string.find(tag, "%d%d月") or string.find(tag, "%d月") then break end 393 | if string.find(tag, animeName) then break end 394 | if string.find(animeName, tag) then break end 395 | local contains = false 396 | for __, ct in ipairs(containRemoveTags) do 397 | if string.find(tag, ct) then 398 | contains = true 399 | break 400 | end 401 | end 402 | if contains then break end 403 | table.insert(fTags, tag) 404 | until true 405 | end 406 | local tagSet = Set(fTags) 407 | removeWhen(tagSet, "轻改", {"轻小说改", "小说改", "小说改编"}) 408 | removeWhen(tagSet, "漫改", {"漫画改", "漫画改编"}) 409 | removeWhen(tagSet, "游戏改", {"手游", "手游改", "游戏改编", "GAL改"}) 410 | removeWhen(tagSet, "治愈", {"治愈系"}) 411 | removeWhen(tagSet, "泡面番", {"泡面"}) 412 | removeWhen(tagSet, "萌系", {"萌", "萌豚"}) 413 | removeWhen(tagSet, "卖肉", {"肉", "肉番"}) 414 | removeWhen(tagSet, "肉番", {"肉"}) 415 | removeWhen(tagSet, "续作", {"续篇", "续集"}) 416 | removeWhen(tagSet, "狗粮", {"酸"}) 417 | local retTags = {} 418 | 419 | local i, maxCount = 0, 20 420 | for t, _ in pairs(tagSet) do 421 | table.insert(retTags, unescape(t)) 422 | i = i+1 423 | if i>= maxCount then break end 424 | end 425 | local retTags = addLevelTags(retTags, anime) 426 | return retTags 427 | end 428 | 429 | function gettags(anime) 430 | local bgmId = anime["data"] 431 | local err, reply = kiko.httpget(string.format("http://bgm.tv/subject/%s", bgmId)) 432 | if err ~= nil then error(err) end 433 | local content = reply["content"] 434 | local _, _, tagContent = string.find(content, "
    (.*)
    ") 435 | local tags = {} 436 | if tagContent ~= nil then 437 | local parser = kiko.htmlparser(tagContent) 438 | while not parser:atend() do 439 | if parser:curnode()=="a" and parser:start() then 440 | parser:readnext() 441 | table.insert(tags, parser:readcontent()) 442 | end 443 | parser:readnext() 444 | end 445 | end 446 | return tagFilter(tags, anime) 447 | end -------------------------------------------------------------------------------- /library/douban.lua: -------------------------------------------------------------------------------- 1 | -- 豆瓣电影 Scraper 2 | -- KikoPlay 资料脚本,从豆瓣电影获取相关信息 3 | -- 4 | -- 关于自动关联,建议的[电视剧]目录层级以及命名方式 (KikoPlay 2.0开始文件识别脚本与资料脚本分离,这个脚本中的match函数不会被调用) 5 | -- > 我有一个朋友 6 | -- -- 01.mp4 7 | -- -- 02.mp4 8 | -- -- ... 9 | -- -- 番外.mp4 10 | -- > 克拉克森的农场 11 | -- -- S01E01.mp4 12 | -- -- S01E02.mp4 13 | -- -- ... 14 | -- -- S02E02.mp4 15 | -- -- ... 16 | 17 | ---------------- 18 | -- 公共部分 19 | -- 脚本信息 20 | info = { 21 | ["name"] = "豆瓣电影", 22 | ["id"] = "Kikyou.l.Douban", 23 | ["desc"] = "豆瓣电影脚本,从 movie.douban.com 中获取视频信息", 24 | ["version"] = "0.2", 25 | ["min_kiko"] = "2.0.0" 26 | } 27 | 28 | 29 | local function url_decode(str) 30 | if not str then return "" end 31 | -- 处理加号表示的空格 32 | str = string.gsub(str, "+", " ") 33 | -- 处理百分比编码的字符 34 | str = string.gsub(str, "%%(%x%x)", function(hex) 35 | -- 将16进制字符串转换为字符 36 | return string.char(tonumber(hex, 16)) 37 | end) 38 | return str 39 | end 40 | 41 | -- 解析查询字符串部分 42 | local function parse_query_string(query_str) 43 | local params = {} 44 | if not query_str or query_str == "" then 45 | return params 46 | end 47 | 48 | -- 分割参数对 49 | for pair in string.gmatch(query_str, "[^&]+") do 50 | if pair ~= "" then 51 | -- 分割键和值,处理没有值的参数(如?debug&verbose) 52 | local key, value = string.match(pair, "^([^=]+)=?(.*)$") 53 | if key then 54 | -- 解码键和值 55 | key = url_decode(key) 56 | value = url_decode(value) 57 | 58 | -- 处理多个同名参数的情况 59 | if params[key] then 60 | -- 如果已经存在该键,将其转为数组 61 | if type(params[key]) ~= "table" then 62 | params[key] = {params[key]} 63 | end 64 | table.insert(params[key], value) 65 | else 66 | params[key] = value 67 | end 68 | end 69 | end 70 | end 71 | 72 | return params 73 | end 74 | 75 | -- 解析完整URL并获取查询参数,处理转义字符 76 | local function parse_url_query(url) 77 | if not url or type(url) ~= "string" then 78 | return {} 79 | end 80 | 81 | -- 提取查询字符串部分(问号后面、锚点前面的内容) 82 | local query_str = string.match(url, "?([^#]*)") 83 | 84 | -- 解析查询字符串 85 | return parse_query_string(query_str) 86 | end 87 | 88 | -- HTML实体解码函数 89 | local function html_entity_decode(str) 90 | if not str or type(str) ~= "string" then 91 | return "" 92 | end 93 | 94 | -- 常见的HTML命名实体映射表 95 | local entities = { 96 | ["&"] = "&", -- 和号 97 | ["""] = "\"", -- 双引号 98 | ["'"] = "'", -- 单引号 99 | ["<"] = "<", -- 小于号 100 | [">"] = ">", -- 大于号 101 | [" "] = " ", -- 非换行空格 102 | ["©"] = "©", -- 版权符号 103 | ["®"] = "®", -- 注册商标 104 | ["™"]= "™", -- 商标符号 105 | ["¢"] = "¢", -- 分 106 | ["£"]= "£", -- 英镑 107 | ["¥"] = "¥", -- 日元 108 | ["€"] = "€", -- 欧元 109 | } 110 | 111 | -- 首先替换命名实体 112 | local result = str 113 | for entity, char in pairs(entities) do 114 | result = string.gsub(result, entity, char) 115 | end 116 | 117 | -- 处理十进制实体,如' 118 | result = string.gsub(result, "&#(%d+);", function(num) 119 | return string.char(tonumber(num)) 120 | end) 121 | 122 | -- 处理十六进制实体,如' 123 | result = string.gsub(result, "&#x(%x+);", function(hex) 124 | return string.char(tonumber(hex, 16)) 125 | end) 126 | 127 | return result 128 | end 129 | 130 | 131 | -- 完成搜索功能 ,可选 132 | -- keyword: string,搜索关键字 133 | -- options:table,如果脚本中包含searchsettings,可以通过options["xxx"]获取设置项的值;否则不会传递这个参数 134 | -- 返回:Array[AnimeLite] 135 | -- 需要注意的是,除了下面定义的AnimeLite结构,还可以增加一项eps,类型为Array[EpInfo],包含动画的剧集列表。 136 | function search(keyword, options) 137 | kiko.log("function search") 138 | options = options or {} 139 | local mediais = {} 140 | 141 | local err, reply = kiko.httpget(string.format("https://www.douban.com/search?cat=1002&q=%s", keyword)) 142 | if err ~= nil then error(err) end 143 | local content = reply["content"] 144 | 145 | local result = string.find(content, "no%-result") 146 | if result ~= nil then 147 | table.insert(mediais, { 148 | ["name"] = "没有找到相关信息", 149 | ["data"] = "", 150 | ["extra"] = "换个关键词试试" 151 | }) 152 | return mediais 153 | end 154 | 155 | local _, _, itemListStr = string.find(content, '
    (.-)
    ') 156 | 157 | local itemStrList = string.split(itemListStr, '
    ') 158 | 159 | if itemStrList ~= nil then 160 | for i = 2, #itemStrList do -- 第一条是无意义换行"\n\n \n \n\n ",从第二条开始遍历 161 | local itemStr = itemStrList[i] 162 | 163 | if itemStr ~= "" then 164 | local _, _, tag, url, title = string.find(itemStr, [[

    165 | %s-(.-) 166 | %s- (.-).-

    ]]) 167 | 168 | local _, _, cast = string.find(itemStr, '(.-)') 169 | local extra = "" 170 | if tag ~= nil then 171 | extra = extra .. tag 172 | end 173 | if cast ~= nil then 174 | extra = extra .. '\n' .. cast 175 | end 176 | local _, _, desc = string.find(itemStr, '

    (.-)

    ') 177 | if desc ~= nil then 178 | extra = extra .. '\n' .. desc 179 | end 180 | 181 | local url_query = parse_url_query(url) 182 | if url_query["url"] ~= nil then 183 | url = url_query["url"] 184 | end 185 | local data = { 186 | ["url"] = url, 187 | } 188 | local _, _, subject_id = string.find(url, "subject/(%d+)") 189 | if subject_id ~= nil then 190 | data["subject_id"] = subject_id 191 | end 192 | local err, media_data_json = kiko.table2json(data) 193 | 194 | table.insert(mediais, { 195 | ["name"] = string.trim(title), 196 | ["data"] = media_data_json, 197 | ["extra"] = extra, 198 | }) 199 | end 200 | end 201 | end 202 | 203 | return mediais 204 | end 205 | 206 | function getEpInfo(count) 207 | local eps = {} 208 | for i = 1, count do 209 | local name = "" 210 | 211 | if i < 10 then 212 | name = "0" .. i 213 | else 214 | name = "" .. i 215 | end 216 | table.insert(eps, { 217 | ["name"] = name, --分集名称 218 | ["index"] = i, --分集编号(索引) 219 | ["type"] = 1 --分集类型 220 | }) 221 | end 222 | return eps 223 | end 224 | 225 | -- 获取动画的剧集信息。 226 | -- anime: Anime 227 | -- 返回: Array[EpInfo] 228 | 229 | -- 在调用这个函数时,anime的信息可能不全,但至少会包含name,data这两个字段。 230 | function getep(anime) 231 | kiko.log("function getep") 232 | 233 | if anime.url == "" then 234 | anime = detail(anime) 235 | end 236 | 237 | return anime.eps 238 | end 239 | 240 | -- 获取动画详细信息 241 | -- anime: AnimeLite 242 | -- 返回:Anime 243 | function detail(anime) 244 | kiko.log("function detail") 245 | local err, anime_data = kiko.json2table(anime["data"]) 246 | if err ~= nil or anime_data.url == nil or anime_data.url == "" then 247 | return nil 248 | end 249 | 250 | local url = '' 251 | if anime_data.url ~= nil then 252 | url = anime_data.url 253 | end 254 | if anime_data.subject_id ~= nil then 255 | url = "https://movie.douban.com/subject/" .. anime_data.subject_id .. "/" 256 | end 257 | 258 | local err, reply = kiko.httpget(url) 259 | 260 | if err ~= nil then error(err) end 261 | local content = reply["content"] 262 | 263 | local _, _, dataJsonStr = string.find(content, "") 264 | if dataJsonStr ~= nil then 265 | local err1, data = kiko.json2table(dataJsonStr) 266 | 267 | if err1 ~= nil then 268 | kiko.log("[ERROR] JSON 转换失败" .. dataJsonStr .. ".json2table: " .. err1) 269 | error(err1) 270 | end 271 | 272 | local name = data.name 273 | if string.startswith(name, anime.name) then 274 | name = anime.name -- data.name 可能包含原名和翻译名 275 | end 276 | local anime_detail = { 277 | ["name"] = name, 278 | ["url"] = "https://movie.douban.com" .. data.url, 279 | ["desc"] = data.description, 280 | ["airdate"] = data.datePublished, 281 | ["coverurl"] = data.image, 282 | } 283 | 284 | local staffs = {} 285 | if data.director ~= nil and #data.director > 0 then 286 | local directors = {} 287 | for _, director in ipairs(data.director) do 288 | table.insert(directors, html_entity_decode(director.name)) 289 | end 290 | table.insert(staffs, "导演:" .. table.concat(directors, ", ")) 291 | end 292 | if data.author ~= nil and #data.author > 0 then 293 | local authors = {} 294 | for _, author in ipairs(data.author) do 295 | table.insert(authors, html_entity_decode(author.name)) 296 | end 297 | table.insert(staffs, "编剧:" .. table.concat(authors, ", ")) 298 | end 299 | if data.actor ~= nil and #data.actor > 0 then 300 | local actors = {} 301 | for _, actor in ipairs(data.actor) do 302 | table.insert(actors, html_entity_decode(actor.name)) 303 | end 304 | table.insert(staffs, "主演:" .. table.concat(actors, ", ")) 305 | end 306 | -- staff info 307 | local _, _, staffListStr = string.find(content, '
    (.-)
    ') 308 | if staffListStr ~= nil then 309 | local lpos = 1 310 | local npos = #staffListStr 311 | while lpos < npos do 312 | local _, epos, role, names = string.find(staffListStr, '(.-):(.-)
    ', lpos) 313 | if role ~= nil and names ~= nil then 314 | role = string.trim(role) 315 | names = string.gsub(names, "<.->", " ") 316 | names = string.trim(names) 317 | table.insert(staffs, role .. ":" .. html_entity_decode(names)) 318 | lpos = epos + 1 319 | else 320 | break 321 | end 322 | end 323 | end 324 | if #staffs > 0 then 325 | anime_detail["staff"] = table.concat(staffs, ";") 326 | end 327 | 328 | -- 角色 329 | local _, _, celebritieListStr = string.find(content, 330 | "
      (.-)
    ") 331 | 332 | local celebritieStrList = string.split(celebritieListStr, '
  • ') 333 | 334 | if (celebritieStrList ~= nil) then 335 | local crt = {} 336 | for i = 2, #celebritieStrList do -- 第一条是无意义换行"\n\n \n \n\n ",从第二条开始遍历 337 | local celebritie = celebritieStrList[i] 338 | if celebritie ~= "" then 339 | local _, _, actor = string.find(celebritie, '(.-)') 340 | local c = {} 341 | if actor ~= nil then 342 | c["actor"] = actor 343 | end 344 | 345 | local _, _, link, name = string.find(celebritie, 346 | '(.-)') 347 | 348 | if link ~= nil then 349 | c["link"] = link 350 | end 351 | 352 | if name ~= nil then 353 | c["name"] = name 354 | end 355 | 356 | 357 | local _, _, imgurl = string.find(celebritie, 358 | '
    ') 359 | if imgurl ~= nil then 360 | c["imgurl"] = imgurl 361 | end 362 | 363 | table.insert(crt, c) 364 | end 365 | end 366 | 367 | anime_detail["crt"] = crt 368 | end 369 | 370 | -- 集数 371 | local _, _, epCount = string.find(content, '集数: (%d-)
    ') 372 | if epCount ~= nil then 373 | local count = tonumber(epCount) 374 | anime_detail["epcount"] = tonumber(count) 375 | 376 | if count > 0 then 377 | anime_detail["eps"] = getEpInfo(count) 378 | end 379 | else 380 | anime_detail["epcount"] = 1 381 | anime_detail["eps"] = getEpInfo(1) 382 | end 383 | 384 | -- 标签 385 | local _, _, tagsStr = string.find(content, '类型: (.-)
    ') 386 | if tagsStr ~= nil then 387 | local parser = kiko.htmlparser(tagsStr) 388 | local tags = {} 389 | while not parser:atend() do 390 | if parser:curnode() == "span" and parser:start() and parser:curproperty("property") == "v:genre" then 391 | table.insert(tags, parser:readcontent()) 392 | end 393 | parser:readnext() 394 | end 395 | 396 | anime_data["tags"] = tags 397 | end 398 | 399 | local err, media_data_json = kiko.table2json(anime_data) 400 | anime_detail["data"] = media_data_json 401 | 402 | return anime_detail 403 | end 404 | 405 | return anime 406 | end 407 | 408 | -- KikoPlay支持多级Tag,用"/"分隔,你可以返回类似“动画制作/A1-Pictures”这样的标签 409 | -- anime: Anime 410 | -- 返回: Array[string],Tag列表 411 | function gettags(anime) 412 | kiko.log("function gettags") 413 | local err, anime_data = kiko.json2table(anime["data"]) 414 | if err ~= nil or anime_data.tags == nil or anime_data.tags == "" then 415 | return {} 416 | end 417 | 418 | return anime_data.tags 419 | end 420 | 421 | -- 根据路径获取文件名 422 | -- filterTitlle:是否过滤纯标题 423 | -- 如果文件名是纯数字,使用上级文件夹名字 424 | function getFileTitle(path, filterTitlle) 425 | local levels = string.split(path, '/') 426 | local _, _, fileName = string.find(levels[#levels], "(.+)%.(.-)") 427 | if filterTitlle and (tonumber(fileName) ~= nil or string.indexof(fileName, "番外") ~= -1) and string.indexof(levels[#levels - 1], ":") == -1 then 428 | fileName = levels[#levels - 1] 429 | end 430 | return fileName 431 | end 432 | 433 | -- 实现自动关联功能。提供此函数的脚本会被加入到播放列表的“关联”菜单中 434 | -- path:文件路径 435 | -- 返回:MatchResult 436 | function match(path) 437 | kiko.log("function match") 438 | local fileName = getFileTitle(path, true) 439 | local animeLites = search(fileName) 440 | if #animeLites > 0 and animeLites[1]["data"] ~= "" then 441 | local anime = detail(animeLites[1]) 442 | if anime ~= nil then 443 | local title = getFileTitle(path, false) 444 | local index 445 | if string.find(title, "S%d+E%d+") ~= nil then 446 | local _, _, index1 = string.find(title, "S%d+E(%d+)") 447 | index = tonumber(index1 or 1) 448 | else 449 | local _, _, index2 = string.find(title, "(%d+)") 450 | index = tonumber(index2 or 1) 451 | end 452 | 453 | if anime["epcount"] ~= nil and anime["epcount"] < index then 454 | index = 1 455 | end 456 | 457 | local type = 1 458 | if string.indexof(title, "番外") ~= -1 or string.indexof(title, "特别篇") ~= -1 then 459 | type = 2 460 | end 461 | 462 | return { 463 | ["success"] = true, --是否成功关联 464 | ["anime"] = anime, --关联的动画信息 465 | ["ep"] = { --关联的剧集信息 466 | ["name"] = title, --分集名称 467 | ["index"] = index, --分集编号(索引) 468 | ["type"] = type --分集类型 469 | } 470 | } 471 | end 472 | end 473 | end 474 | 475 | -- menus 476 | -- Table,类型为 Array[LibraryMenu] 477 | -- 如果资料库条目的scriptId和当前脚本的id相同,条目的右键菜单中会添加menus包含的菜单项,用户点击后会通过menuclick函数通知脚本 478 | 479 | -- menuid: string,点击的菜单ID 480 | -- anime: Anime, 条目信息 481 | 482 | --function menuclick(menuid, anime) 483 | -- kiko.log("menuclick") 484 | -- -- kiko.log(kiko.table2json(menuid)) 485 | -- -- kiko.log(kiko.table2json(anime)) 486 | --end 487 | -------------------------------------------------------------------------------- /reference.md: -------------------------------------------------------------------------------- 1 | # KikoPlay 脚本开发参考 2 | 2025.08 By Kikyou,本文档适用于KikoPlay 2.0.0及以上版本 3 | 4 | ## 目录 5 | - [脚本类型](#脚本类型) 6 | - [公共部分](#公共部分) 7 | - [弹幕脚本](#弹幕脚本) 8 | - [文件识别脚本](#文件识别脚本) 9 | - [资料脚本](#资料脚本) 10 | - [资源脚本](#资源脚本) 11 | - [番组日历脚本](#番组日历脚本) 12 | - [KikoPlay API](#kikoplay-api) 13 | - [数据类型](#数据类型) 14 | - [DanmuSource](#danmusource) 15 | - [DanmuComment](#danmucomment) 16 | - [AnimeLite](#animelite) 17 | - [EpInfo](#epinfo) 18 | - [Character](#character) 19 | - [Anime](#anime) 20 | - [MatchResult](#matchresult) 21 | - [LibraryMenu](#librarymenu) 22 | - [ResourceItem](#resourceitem) 23 | - [NetworkReply](#networkreply) 24 | - [BgmSeason](#bgmseason) 25 | - [BgmItem](#bgmitem) 26 | 27 | 28 | ## 脚本类型 29 | KikoPlay中有5类脚本: 30 | - 弹幕脚本: 位于script/danmu目录下,提供弹幕搜索、下载功能 31 | - 文件识别脚本:位于script/match目录下,提供文件识别功能,将文件识别到动画的某个剧集上,2.0.0新增 32 | - 资料脚本:位于script/library目录下,提供动画(或者其他类型的条目)搜索、详细信息获取、分集信息获取、标签获取功能 33 | - 资源脚本:位于script/resource目录下,提供资源搜索功能 34 | - 番组日历脚本:位于script/bgm_calendar,提供每日放送列表。0.8.2起新增 35 | 36 | 所有的脚本均为Lua脚本,不同类型的脚本需要提供不同的接口 37 | ### 公共部分 38 | 每个脚本都应包含一个`info`类型的table,提供脚本的基本信息,包含这些内容: 39 | ```lua 40 | info = { 41 | ["name"] = "Bilibili", --脚本名称 42 | ["id"] = "Kikyou.d.Bilibili", --脚本id,不应和其他脚本的id相同 43 | ["desc"] = "Bilibili弹幕脚本", --描述信息 44 | ["version"] = "0.1", --版本信息 45 | ["min_kiko"] = "0.9.1", --可选,0.9,1起新增,最低要求的KikoPlay版本 46 | ["label_color"] = "0xDC478A", --可选,弹幕脚本标签颜色,2.0.0新增 47 | } 48 | ``` 49 | 脚本可以包含设置项,这些项目可以通过KikoPlay “设置”对话框-“脚本”页面-脚本列表的右键菜单-“设置” 进行设置 50 | 设置项目包含在脚本的`settings` table中,其中每一项的key为设置项的key,value是一个table,格式如下: 51 | ```lua 52 | settings = { 53 | ["result_type"] = { 54 | ["title"]="搜索结果类型", --设置项标题 55 | ["default"]="2", --默认值,类型均为字符串 56 | ["desc"]="条目类型, 2: 动画 3:三次元", --描述信息 57 | ["choices"]="2,3", --可选,如果包含这一项,用户只能从choices中进行选择,多个项目用逗号“,”分隔 58 | ["group"]="xxx" --可选,0.9.1新增,设置group后KikoPlay会在设置页中聚合相同group的选项 59 | } 60 | } 61 | ``` 62 | KikoPlay在加载脚本后,会将value替换为设置的值,例如上面的示例在加载后,可能会变成: 63 | ```lua 64 | settings = { 65 | ["result_type"] = "2" 66 | } 67 | ``` 68 | 因此在脚本中,可以直接通过`settings["xxxx"]`获取设置项的值,如果需要在脚本中修改设置项的值并保存,可以使用KikoPlay提供的`writesetting`函数 69 | 70 | 注意,设置项值的类型都是字符串。如果用户在脚本加载后,从KikoPlay的设置对话框中修改了脚本设置项,KikoPlay会尝试调用脚本的`setoption`函数通知脚本,如果需要对修改进行响应,可以在脚本中添加这个函数: 71 | ```lua 72 | function setoption(key, val) 73 | -- key为设置项的key,val为修改后的value 74 | end 75 | ``` 76 | 0.9.0开始,脚本可以在 设置页 脚本列表 的右键菜单中添加自定义项目。在脚本中添加`scriptmenus` table,例如: 77 | ```lua 78 | scriptmenus = { 79 | {["title"]="", ["id"]="about"} 80 | } 81 | ``` 82 | `scriptmenus`是一个数组,每个元素包含`title`和`id`,其中`title`为展示的菜单文本,`id`用于标识菜单项,当用户点击菜单时,在`scriptmenuclick`中响应: 83 | ```lua 84 | function scriptmenuclick(menuid) 85 | if menuid == "about" then 86 | kiko.dialog({ 87 | ["title"]="KikoPlay脚本菜单测试", 88 | ["tip"]="这是来自KikoPlay的测试" 89 | }) 90 | end 91 | end 92 | ``` 93 | 0.9.1起新增了搜索设置功能,方便用户在搜索时快速设置某些选项,格式如下: 94 | ```lua 95 | searchsettings = { 96 | ["result_type"]={ 97 | ["title"]="搜索结果类型", --标题 98 | ["default"]="动画", --可选,默认值 99 | ["desc"]="搜索结果类型", --描述信息 100 | ["choices"]="书籍,动画,音乐,游戏,三次元", --可选,选项 101 | ["save"]=true, --可选,是否保存值,bool类型,默认为true,下次搜索时会保存上次的值 102 | ["display_type"] = 2 --展示类型,整数,默认为0(文本) 103 | }, 104 | } 105 | ``` 106 | `display_type`的取值如下: 107 | ``` 108 | Text = 0 文本框 109 | Combo = 1 下拉列表 110 | Radio = 2 单选列表 111 | Check = 3 单选框,如果display_type设为单选框,这个设置项的值只会传递为"0"/"1" 112 | CheckList = 4 多选列表,如果用户选了多个值,会用','拼接传递到脚本 113 | Label = 5 仅展示标题 114 | ``` 115 | `searchsettings`的值会在搜索函数中传递到脚本 116 | ### 弹幕脚本 117 | 弹幕脚本需要包含如下函数/table: 118 | - `function search(keyword, )` 119 | 120 | > `keyword`: string,搜索关键字 121 | > 122 | > `options`:table,如果脚本中包含`searchsettings`,可以通过`options["xxx"]`获取设置项的值;否则不会传递这个参数 123 | > 124 | > 返回:Array[[DanmuSource](#danmusource)] 125 | 126 | 完成搜索功能,可选 127 | 128 | - `function epinfo(source)` 129 | 130 | > `source`: [DanmuSource](#danmusource) 131 | > 132 | > 返回:Array[[DanmuSource](#danmusource)] 133 | 134 | 返回source条目包含的分集列表,分集条目也是DanmuSource 135 | 136 | - `function danmu(source)` 137 | 138 | > `source`: [DanmuSource](#danmusource) 139 | > 140 | > 返回:[DanmuSource](#danmusource)/nil, Array[[DanmuComment](#danmucomment)] 141 | 142 | 从DanmuSource获取弹幕。如果在获取弹幕后source发生变化,第一个返回值为新的source,否则第一个返回值为nil,第二个值返回弹幕列表 143 | 144 | - `supportedURLsRe` 145 | 146 | > Table,类型为 Array[string],支持的弹幕URL正则表达式列表,可选 147 | 148 | 脚本提供的`supportedURLsRe`正则表达式会在“从URL添加弹幕”功能中对用户输入的URL进行过滤,符合条件的URL将被传入脚本,需要脚本提供`urlinfo`函数 149 | 150 | 例如,bilibili.lua支持的URL正则表达式包括: 151 | ```lua 152 | supportedURLsRe = { 153 | "(https?://)?www\\.bilibili\\.com/video/av[0-9]+/?", 154 | "(https?://)?www\\.bilibili\\.com/video/BV[\\dA-Za-z]+/?", 155 | "av[0-9]+", 156 | "BV[\\dA-Za-z]+", 157 | "(https?://)?www\\.bilibili\\.com/bangumi/media/md[0-9]+/?" 158 | } 159 | ``` 160 | 161 | - `sampleSupporedURLs` 162 | 163 | > Table,类型为 Array[string],支持的URL示例,可选,会显示在KikoPlay添加弹幕对话框-URL页面-支持的URL类型列表中,主要用于提示用户 164 | 165 | - `function urlinfo(url)` 166 | 167 | > `url`: string 168 | > 169 | > 返回:[DanmuSource](#danmusource) 170 | 171 | 从`url`中获取弹幕来源信息 172 | 173 | ### 文件识别脚本 174 | 文件识别脚本需要提供`match`函数。 175 | 176 | 尽管下文使用“动画”一词,但视频类型并不局限于动画。 177 | - `function match(path)` 178 | 179 | > `path`:文件路径 180 | > 181 | > 返回:[MatchResult](#matchresult) 182 | 183 | 识别文件所属动画的剧集。 184 | 185 | ### 资料脚本 186 | 资料脚本需要提供`search`和`getep`函数。 187 | 188 | 尽管下文使用“动画”一词,但视频类型并不局限于动画。 189 | - `function search(keyword, )` 190 | 191 | > `keyword`: string,搜索关键字 192 | > 193 | > `options`:table,如果脚本中包含`searchsettings`,可以通过`options["xxx"]`获取设置项的值;否则不会传递这个参数 194 | > 195 | > 返回:Array[[AnimeLite](#animelite)] 196 | > 197 | > 完成搜索功能,可选 198 | 199 | 需要注意的是,除了下面定义的AnimeLite结构,还可以增加一项`eps`,类型为Array[[EpInfo](#epinfo)],包含动画的剧集列表。 200 | 201 | - `function getep(anime)` 202 | 203 | > `anime`: [Anime](#anime) 204 | > 205 | > 返回: Array[[EpInfo](#epinfo)] 206 | 207 | 获取动画的剧集信息。在调用这个函数时,`anime`的信息可能不全,但至少会包含`name`,`data`这两个字段。 208 | 209 | - `function detail(anime)` 210 | 211 | > `anime`: [AnimeLite](#animelite) 212 | > 213 | > 返回:[Anime](#anime) 214 | 215 | 获取动画详细信息 216 | 217 | - `function gettags(anime)` 218 | 219 | > `anime`: [Anime](#anime) 220 | > 221 | > 返回: Array[string],Tag列表 222 | 223 | KikoPlay支持多级Tag,用"/"分隔,你可以返回类似“动画制作/A1-Pictures”这样的标签 224 | 225 | - `menus` 226 | 227 | > Table,类型为 Array[[LibraryMenu](#librarymenu)] 228 | 229 | 如果资料库条目的scriptId和当前脚本的id相同,条目的右键菜单中会添加`menus`包含的菜单项,用户点击后会通过`menuclick`函数通知脚本 230 | 231 | - `function menuclick(menuid, anime)` 232 | 233 | > `menuid`: string,点击的菜单ID 234 | > 235 | > `anime`: [Anime](#anime), 条目信息 236 | 237 | 返回:无 238 | 239 | ### 资源脚本 240 | 资源脚本提供资源搜索功能(主要是bt类资源) 241 | 242 | - `function search(keyword, page, scene, )` 243 | 244 | > `keyword`: string,搜索关键字 245 | > 246 | > `page`: 页码 247 | > 248 | > `scene`: 搜索场景,目前有两个:"search"和"auto-download",后者表示在自动下载功能中KikoPlay调用脚本进行搜索 249 | > 250 | > `options`:table,如果脚本中包含`searchsettings`,可以通过`options["xxx"]`获取设置项的值;否则不会传递这个参数 251 | > 252 | > 返回: Array[[ResourceItem](#resourceitem)] 253 | 254 | 当`scene=="auto-download"`时,脚本不应在搜索中使用dialog等函数阻塞脚本,等待用户输入 255 | - `function getdetail(item)` 256 | 257 | > `item`: [ResourceItem](#resourceitem) 258 | > 259 | > 返回: [ResourceItem](#resourceitem),包含`magnet`字段的item信息 260 | 261 | 可选,如果在搜索中无法确定资源的`magnet`信息,脚本需要提供`getdetail`函数获取详细信息。 262 | 263 | ### 番组日历脚本 264 | 此类脚本用于提供番组日历。 0.9.0开始,KikoPlay支持多个番组日历 265 | 266 | - `function getseason()` 267 | 268 | > 返回:Array[[BgmSeason](#bgmseason)] 269 | 270 | 获取番组分季列表,例如2021-01, 2021-04, 2021-07,... 271 | 272 | - `function getbgmlist(season)` 273 | 274 | > season:[BgmSeason](#bgmseason) 275 | > 276 | > 返回: Array[[BgmItem](#bgmitem)] 277 | 278 | 获取season下的番剧列表 279 | 280 | 281 | ## KikoPlay API 282 | 283 | KikoPlay提供的API位于kiko表中,通过kiko.xxx调用 284 | 285 | - `httpget(url, query, header, redirect)` 286 | 287 | > `url`:string 288 | > 289 | > `query`:查询,`{[key]=value,...}`,可选,默认为空 290 | > 291 | > `header`: HTTP Header, `{[key]=value,...}`,可选,默认为空 292 | > 293 | > `redirect`:bool,是否自动进行重定向,默认`true` 294 | > 295 | > 返回:string/nil, [NetworkReply](#networkreply) 296 | 297 | 发送HTTP GET请求。返回的第一个值表示是否发生错误,没有错误时为nil,否则是错误信息 298 | 299 | - `httpgetbatch(urls, querys, headers, redirect)` 300 | 301 | > `urls`: Array[string] 302 | > 303 | > `querys`:查询,`Array[{[key]=value,...}]`,可选,默认为空 304 | > 305 | > `headers`: HTTP Header, `Array[{[key]=value,...}]`,可选,默认为空 306 | > 307 | > `redirect`:bool,是否自动进行重定向,默认`true` 308 | > 309 | > 返回:string/nil, Array[[NetworkReply](#networkreply)] 310 | 311 | 和`httpget`类似,但可以一次性发出一组HTTP Get请求,需要确保`urls`、`querys`和`headers`中的元素一一对应,querys和headers也可以为空 312 | 313 | - `httppost(url, data, header, querys)` 314 | 315 | > `url`:string 316 | > 317 | > `data`:string, POST数据 318 | > 319 | > `header`: HTTP Header, `{[key]=value,...}`,可选,默认为空 320 | > 321 | > `querys`:查询,`Array[{[key]=value,...}]`,可选,默认为空 322 | > 323 | > 返回:string/nil, [NetworkReply](#networkreply) 324 | 325 | 发送HTTP POST请求。返回的第一个值表示是否发生错误,没有错误时为nil,否则是错误信息 326 | 327 | - `httphead(url, query, header, redirect)` 328 | 329 | > `url`:string 330 | > 331 | > `query`:查询,`{[key]=value,...}`,可选,默认为空 332 | > 333 | > `header`: HTTP Header, `{[key]=value,...}`,可选,默认为空 334 | > 335 | > `redirect`:bool,是否自动进行重定向,默认`true` 336 | > 337 | > 返回:string/nil, [NetworkReply](#networkreply) 338 | 339 | 发送HTTP Head请求。返回的第一个值表示是否发生错误,没有错误时为nil,否则是错误信息 340 | 341 | - `json2table(jsonstr)` 342 | 343 | > `jsonstr`:string, json字符串 344 | > 345 | > 返回:string/nil, Table 346 | 347 | 将json字符串解析为lua的Table 348 | 返回的第一个值表示是否发生错误,没有错误时为nil,否则是错误信息 349 | 350 | - `table2json(table, compact)` 351 | 352 | > `table`:Table, 待转换为json的table 353 | > 354 | > `compact`: string, 可选,表示输出紧凑还是格式化的json,默认为格式化的json,传入"compact"则输出紧凑的json 355 | > 356 | > 返回:string/nil, string 357 | 358 | 将lua的Table转换为json字符串 359 | 返回的第一个值表示是否发生错误,没有错误时为nil,否则是错误信息 360 | 361 | - `compress(content, type)` 362 | 363 | > `content`:string, 待压缩的字符串 364 | > 365 | > `type`:压缩方式,可选,默认为gzip,目前也只支持gzip 366 | > 367 | > 返回:string/nil, string 368 | 369 | 压缩字符串, 第二个返回值为压缩结果 370 | 371 | 返回的第一个参数表示是否发生错误,没有错误时为nil,否则是错误信息 372 | 373 | - `decompress(content, type)` 374 | 375 | > `content`:string, 待压缩的字符串 376 | > 377 | > `type`:压缩方式,可选,支持inflate和gzip,默认为inflate 378 | > 379 | > 返回:string/nil, string 380 | 381 | 解压缩字符串, 第二个返回值为解压缩结果 382 | 383 | 返回的第一个值表示是否发生错误,没有错误时为nil,否则是错误信息 384 | 385 | - `execute(detached, program, args)` 386 | 387 | > `detached`:bool,是否等待程序执行结束,true:不等待 388 | > 389 | > `program`:string,执行的程序 390 | > 391 | > `args`: Array[string], 参数列表 392 | > 393 | > 返回:string/nil, bool/number 394 | 395 | 执行外部程序。返回的第一个值表示调用参数是否正确,没有错误时为nil,否则是错误信息。第二个值为程序执行结果,如果`detached=true`,值为true/false表示程序是否启动;如果`detached=false`,值为外部程序的返回值 396 | 397 | - `hashdata(path_data, ispath, filesize, algorithm`) 398 | 399 | > `path_data`:string,文件路径或者待计算hash的数据 400 | > 401 | > `ispath`:bool,第一个参数是否是文件路径,默认=true 402 | > 403 | > `filesize`: number,只有第一个参数为文件路径才有意义,表示读取文件的大小,=0表示读取整个文件,否则只读取前`filesize` bytes 404 | > 405 | > `algorithm`:string,hash算法,默认为md5,可选:md4,md5,sha1,sha224,sha256,sha386,sha512 406 | > 407 | > 返回:string/nil, string 408 | 409 | 计算文件或者数据的hash,第一个返回值表示是否出错,第二个返回值为hash值 410 | 411 | - `base64(data, type)` 412 | 413 | > `data`:string,待转换或者已经base64编码的数据 414 | > 415 | > `type`:string, 可选from/to, from:base64解码,to: base64编码,默认为from 416 | > 417 | > 返回:string/nil, string 418 | 419 | 0.9.0新增,base64转换函数,第一个返回值表示是否出错,第二个返回值为base64编码/解码结果 420 | 421 | - `log(...)` 422 | 423 | 打印输出到KikoPlay的“脚本日志”对话框中。支持多个参数,如果只有一个参数且类型为Table,会以json的形式将整个Table的内容输出(注意,Table不能包含循环引用) 424 | 425 | - `writesetting(key, value)` 426 | 427 | > `key`:string,设置项的key 428 | > 429 | > `value`:string, 设置项的值 430 | > 431 | > 返回:string/nil, nil 432 | 433 | 修改设置项的值,改动会被保存。第一个返回值表示是否出错 434 | 435 | - `viewtable(table)` 436 | 437 | 0.9.0新增,可视化Table的全部内容,便于进行调试 438 | 439 | - `message(msg, flags)` 440 | 441 | > `msg`:string,消息内容 442 | > 443 | > `flags`:number,标志,默认为1(NM_HIDE),多个标志使用 | 运算,其他的标志有: 444 | > ``` 445 | > NM_HIDE=1 一段时间后自动隐藏 446 | > NM_PROCESS=2 显示busy动画 447 | > NM_SHOWCANCEL = 4 显示cancel按钮 448 | > NM_ERROR = 8 错误信息 449 | > NM_DARKNESS_BACK = 16 显示暗背景,阻止用户执行其他操作 450 | > ``` 451 | > 452 | 453 | 目前只支持资料脚本在“资料”页面顶部弹出消息,NM_SHOWCANCEL在这里不起作用。如果没有NM_HIDE标志,弹出的消息会一直显示,直到下一个消息出现。 454 | 455 | - `dialog(dialog_config)` 456 | 457 | > `dialog_config`:Table,配置对话框显示内容,内容包括: 458 | > ```lua 459 | > { 460 | > ["title"]=string, --对话框标题,可选 461 | > ["tip"]=string, --对话框提示信息 462 | > ["text"]=string, --可选,存在这个字段将在对话框显示一个可供输入的文本框,并设置text为初始值 463 | > ["image"]=string --可选,内容为图片数据,存在这个字段将在对话框内显示图片 464 | > } 465 | > ``` 466 | > 返回:bool,string 467 | 468 | 展示一个对话框,第一个返回值表示用户点击接受(true)还是直接关闭(false),第二个返回值为用户输入的文本 469 | 470 | - `sttrans(str, to_simp)` 471 | 472 | > `str`:string,源字符串 473 | > 474 | > `to_simp`:bool,是否转换为简体中文,true:转换为简体中文,false:转换为繁体中文 475 | > 476 | > 返回:string/nil, string 477 | 478 | 简繁转换,这个函数只有在windows系统上有效,其他平台上会原样返回。第一个返回值表示是否出错,第二个返回值为转换后的结果 479 | 480 | - `envinfo()` 481 | 482 | > 返回: Table,包含: 483 | >```lua 484 | > { 485 | > ["os"]=string, --操作系统 486 | > ["os_version"]=string, --系统版本 487 | > ["kikoplay"]=string, --KikoPlay版本 488 | > ["data_path"]=string, --KikoPlay数据目录 489 | > ["kservice"]=boolean, --是否包含KService,如果包含=true,不包含则没有这一项 490 | > } 491 | > 492 | 493 | - `allscripts()` 494 | 495 | > 返回: Array[[ScriptInfo](#scriptinfo)]: 496 | 497 | 获取KikoPlay中安装的全部脚本,1.0.3起新增 498 | 499 | - `xmlreader(str)` 500 | 501 | > `str`:string,xml内容 502 | > 503 | > 返回:kiko.xmlreader 504 | 505 | 创建一个流式XML读取器。KikoPlay提供了一个简单的XML读取器(封装Qt中的QXmlStreamReader),kiko.xmlreader提供如下方法: 506 | ```lua 507 | adddata(str) --继续添加xml数据 508 | clear() --清空数据 509 | atend() --读取是否到达末尾,true/false 510 | readnext() --读取下一个标签 511 | startelem() --当前是否是开始标签,true/false 512 | endelem() --当前是否是结束标签,true/false 513 | name() --返回当前标签名称 514 | attr(attr_name) --返回属性attr_name的值 515 | hasattr(attr_name) --当前标签是否包含attr_name属性,true/false 516 | elemtext() --读取从当前开始标签到结束标签之间的文本并返回 517 | error() --返回错误信息,没有错误返回nil 518 | ``` 519 | 一个示例(来自danmu/iqiyi.lua): 520 | ```lua 521 | local xmlreader = kiko.xmlreader(danmuContent) 522 | local curDate, curText, curTime, curColor, curUID = nil, nil, nil, nil, nil 523 | while not xmlreader:atend() do 524 | if xmlreader:startelem() then 525 | if xmlreader:name()=="contentId" then 526 | curDate = string.sub(xmlreader:elemtext(), 1, 10) 527 | elseif xmlreader:name()=="content" then 528 | curText = xmlreader:elemtext() 529 | elseif xmlreader:name()=="showTime" then 530 | curTime = tonumber(xmlreader:elemtext()) * 1000 531 | elseif xmlreader:name()=="color" then 532 | curColor = tonumber(xmlreader:elemtext(), 16) 533 | elseif xmlreader:name()=="uid" then 534 | curUID = "[iqiyi]" .. xmlreader:elemtext() 535 | end 536 | elseif xmlreader:endelem() then 537 | if xmlreader:name()=="bulletInfo" then 538 | table.insert(danmuList, { 539 | ["text"]=curText, 540 | ["time"]=curTime, 541 | ["color"]=curColor, 542 | ["date"]=curDate, 543 | ["sender"]=curUID 544 | }) 545 | end 546 | end 547 | xmlreader:readnext() 548 | end 549 | ``` 550 | 551 | - `htmlparser(str)` 552 | 553 | > `str`:string,html内容 554 | > 555 | > 返回:kiko.htmlparser 556 | 557 | 创建一个流式HTML读取器。KikoPlay提供了一个简单的HTML读取器,可以顺序解析HTML标签。kiko.htmlparser提供如下方法: 558 | ```lua 559 | adddata(str) --继续添加html数据 560 | seekto(pos) --跳转到pos位置 561 | atend() --读取是否到达末尾,true/false 562 | readnext() --读取下一个标签 563 | curpos() --返回当前位置 564 | readcontent() --读取内容并返回,直到遇到结束标签 576 | if parser:curnode()=="a" and parser:start() then 577 | parser:readnext() 578 | table.insert(tags, parser:readcontent()) 579 | end 580 | parser:readnext() 581 | end 582 | ``` 583 | 584 | - `regex(str, option)` 585 | 586 | > `str`:string,正则表达式内容 587 | > 588 | > `option`:string,可选,包含i,m,s,x四个选项,可多选: 589 | > - i: CaseInsensitiveOption 590 | > - s: DotMatchesEverythingOption 591 | > - m: MultilineOption 592 | > - x: ExtendedPatternSyntaxOption 593 | > 594 | > 返回:kiko.regex 595 | 596 | 0.9.0新增,封装了`QRegularExpression`,提供了比lua自带的更为高级的正则表达式。kiko.regex提供如下方法: 597 | ```lua 598 | find(target, initpos) 599 | --用当前表达式从initpos位置匹配一次目标字符串target,如果有匹配,返回 起始位置,结束位置,捕获组1,捕获组2,...,可代替Lua原生的string.find()。如果没有匹配,函数什么都不返回 600 | gmatch(target) 601 | --用当前表达式无限次匹配目标字符串,返回Lua风格迭代器,迭代时每次输出当次匹配结果,包括表达式完整匹配(首个返回值)和所有捕获组匹配到的内容,从Lua原生的string.gmatch()迁移则注意是否需要跳过首个返回值 602 | gsub(target, repl) 603 | --用当前表达式对目标字符串无限次执行替换操作,返回替换后的字符串,可接受字符串,表格({[key]=value,...})和函数格式的替换值,返回替换后的结果 604 | setpattern(pattern, options) 605 | --重新设置正则表达式,参数含义和构造函数相同 606 | ``` 607 | 简单示例: 608 | ```lua 609 | local reg = kiko.regex("(\\w+)\\s*(\\w+)") 610 | local i, j , w, f= reg:find("hello world from Lua", 7) 611 | print(("start: %d, end: %d"):format(i, j)) -- start: 7, end: 16 612 | print(w, f) -- world from 613 | 614 | reg:setpattern("\\$(.*?)\\$") 615 | local x = reg:gsub("4+5 = $return 4+5$", function (o, s) 616 | print("in gsub: ", o, s) -- in gsub: $return 4+5$ return 4+5 617 | return load(s)() 618 | end) 619 | print("gsub: ", x) -- gsub: 4+5 = 9 620 | 621 | local x = reg:gsub("4+5 = $ret$", "99") 622 | print("gsub: ", x) -- gsub: 4+5 = 99 623 | 624 | reg:setpattern("(\\w)(\\w)(\\w)\\s(.+)") 625 | local x = reg:gsub("abc abc", {a="Ki", b="ko", c="Play", abc="0.9.0"}) 626 | print("table gsub: ", x) -- table gsub: KikoPlay 0.9.0 627 | 628 | local s = "hello world from Lua" 629 | reg:setpattern("\\w+") 630 | for w in reg:gmatch(s) do 631 | print("gmatch: ", w) 632 | end 633 | ``` 634 | 635 | 1.0.0起增加了一些常用的字符串函数,位于`string`中: 636 | - `trim(str)` 637 | > `str`:string 638 | > 639 | > 返回: 去除`str`两边空白字符后的结果 640 | 641 | 去除字符串两边的空白。 642 | 643 | - `startswith(str, token)` 644 | > `str`:string 645 | > 646 | > `token`:string 647 | > 648 | > 返回: bool 649 | 650 | 判断`token`是否为`str`的前缀。 651 | 652 | - `endswith(str, token)` 653 | > `str`:string 654 | > 655 | > `token`:string 656 | > 657 | > 返回: bool 658 | 659 | 判断`token`是否为`str`的后缀。 660 | 661 | - `split(str, token, skip_empty)` 662 | > `str`:string 663 | > 664 | > `token`:string 665 | > 666 | > `skip_empty`:bool,可选,默认false 667 | > 668 | > 返回: array of string 669 | 670 | 将`str`按照`token`切分,如果`skip_empty=true`,忽略切分中产生的空字符串。 671 | 672 | - `indexof(str, target, from)` 673 | > `str`:string 674 | > 675 | > `target`:string 676 | > 677 | > `from`:integer,可选,默认从1开始 678 | > 679 | > 返回: integer 680 | 681 | 返回`target`在`str`中从`from`开始首次出现的位置,如果没有返回-1。 682 | 683 | - `lastindexof(str, target, from)` 684 | > `str`:string 685 | > 686 | > `target`:string 687 | > 688 | > `from`:integer,可选,默认从-1开始向前搜索 689 | > 690 | > 返回: integer 691 | 692 | 返回`target`在`str`中从`from`开始从后向前首次出现的位置,如果没有返回-1。 693 | 694 | - `encode(str, src_code, dest_code)` 695 | > `str`:string 696 | > 697 | > `src_code`:源编码 698 | > 699 | > `dest_code`:目标编码 700 | > 701 | > 返回: string 702 | 703 | 将`str`从源编码转为目标编码。编码目前有两种取值: 704 | ```lua 705 | string.CODE_LOCAL -- 本地编码 706 | string.CODE_UTF8 -- utf-8编码 707 | ``` 708 | 709 | 1.0.1版本提供了一些操作文件和目录的函数,位于`kiko.dir`中: 710 | - `function fileinfo(path)` 711 | > `path`:string,路径 712 | > 713 | > 返回:table 714 | 715 | 获取文件或目录信息。 716 | 717 | - `function exists(path)` 718 | > `path`:string,路径 719 | > 720 | > 返回:true/false 721 | 722 | 文件或目录是否存在。 723 | 724 | - `function mkpath(path)` 725 | > `path`:string,路径 726 | > 727 | > 返回:true/false 728 | 729 | 创建目录。 730 | 731 | - `function rmpath(path)` 732 | > `path`:string,路径 733 | > 734 | > 返回:true/false 735 | 736 | 删除目录,需要目录为空。 737 | 738 | - `function rename(old_path, new_path)` 739 | > `old_path`:string,之前的路径 740 | > 741 | > `new_path`:string,新路径 742 | > 743 | > 返回:true/false 744 | 745 | 重命名文件/目录。 746 | 747 | - `function syspath()` 748 | > 返回:table,kv形式 749 | 750 | 获取系统路径。 751 | 752 | - `function entrylist(path, namefilter, filter, sort)` 753 | > `path`:string,路径 754 | > 755 | > `namefilter`:string,文件名过滤,可选 756 | > 757 | > `filter`:integer,过滤器,可选 758 | > 759 | > `sort`:integer,排序规则,可选 760 | > 761 | > 返回:array of string 762 | 763 | 获取`path`下的文件和目录。`namefilter`支持通配符,多个过滤规则用`;`隔开,它们是或的关系,例如:"*.cpp;*.cxx;*.cc"。 764 | 765 | `filter`类型如下,可用或组合: 766 | ```lua 767 | kiko.dir.FILTER_DIRS 768 | kiko.dir.FILTER_ALL_DIRS 769 | kiko.dir.FILTER_FILES 770 | kiko.dir.FILTER_DRIVES 771 | kiko.dir.FILTER_NO_SYMLINKS 772 | kiko.dir.FILTER_NO_DOT 773 | kiko.dir.FILTER_ALL_ENTRIES 774 | kiko.dir.FILTER_HIDDEN 775 | ``` 776 | `sort`类型如下,可用或组合: 777 | ```lua 778 | kiko.dir.SORT_NAME 779 | kiko.dir.SORT_TIME 780 | kiko.dir.SORT_SIZE 781 | kiko.dir.SORT_TYPE 782 | kiko.dir.SORT_NO 783 | kiko.dir.SORT_DIR_FIRST 784 | kiko.dir.SORT_DIR_LAST 785 | kiko.dir.SORT_REVERSE 786 | kiko.dir.SORT_IGNORE_CASE 787 | ``` 788 | 789 | 2.0.0引入浏览器对象,底层基于QWebEngineView,通过`kiko.browser.create`函数创建一个browser对象。 790 | - `function create()` 791 | > 792 | > 返回:browser对象 793 | 794 | 另外几个位于`kiko.browser`的函数: 795 | - `function cookie(domain, path)` 796 | > `domain`: string,获取指定domain的cookie,或者为空(获取全部cookie) 797 | > 798 | > `path`: string,获取指定domain下具体path的cookie,或者为空(获取domain全部cookie) 799 | > 800 | > 返回:string/table 801 | 802 | 获取cookie,根据是否设置domain/path返回table或string:string(指定了domain和path);table: {path: cookie, ...} (仅仅指定domain);table: {domain: {path: cookie, ...}} (未指定domain和path) 803 | 804 | - `function ua()` 805 | > 806 | > 返回:string 807 | 808 | 获取浏览器对象的user agent 809 | 810 | - `function setua(user_agent)` 811 | > `user_agent`: string 812 | > 813 | > 返回:空 814 | 815 | 设置浏览器对象的user agent,影响全部实例 816 | 817 | browser对象方法: 818 | - `function load(url, params, timeout)` 819 | > `url`: string,地址 820 | > 821 | > `params`:table,url query参数,可以为空 822 | > 823 | > `timeout`:number,超时时间,可以为空,默认15s 824 | > 825 | > 返回:bool,是否加载成功 826 | 827 | 阻塞加载url 828 | 829 | - `function html()` 830 | > 831 | > 返回:string,当前页面html 832 | 833 | 获取当前页面html 834 | 835 | - `function runjs(js)` 836 | > `js`: string,要执行的js代码 837 | > 838 | > 返回:js代码返回结果 839 | 840 | 在当前页面阻塞执行js并返回结果 841 | 842 | - `function show(tip)` 843 | > `tip`: string,提示信息,可惜 844 | > 845 | > 返回:空 846 | 847 | 弹出窗口显示网页 848 | 849 | 850 | 851 | ## 数据类型 852 | 853 | ### DanmuSource 854 | ```lua 855 | { 856 | ["title"]=string, --条目标题 857 | ["desc"]=string, --条目描述,可选 858 | ["duration"]=number, --时长(s),可选 859 | ["delay"]=number, --延迟(s),可选 860 | ["data"]=string, --可以存放一些条目相关的数据,可选 861 | --当DanmuSource从KikoPlay传递到脚本时,还会提供如下信息 862 | ["scriptId"]=string, --脚本ID 863 | 864 | } 865 | ``` 866 | ### DanmuComment 867 | ```lua 868 | { 869 | ["text"] = string, --弹幕文本 870 | ["time"]=number, --视频时间(ms) 871 | ["color"]=number, --颜色,0xRRGGBB 872 | ["fontsize"]=number, --字体大小,0:正常, 1:小, 2:大 873 | ["type"]=number, --弹幕类型,0:滚动, 1:顶部, 2:底部 874 | ["date"]=string, --发送时间,unix时间戳(s)转换为字符串 875 | ["sender"]=string --发送用户 876 | } 877 | ``` 878 | ### AnimeLite 879 | ```lua 880 | { 881 | ["name"]=string, --动画名称,注意KikoPlay通过name标识动画,name相同即为同一部动画 882 | ["data"]=string, --脚本可以自行存放一些数据 883 | ["extra"]=string, --附加显示数据,这个信息不会由KikoPlay传递到脚本,仅用户向用户展示 884 | ["scriptId"]=string --脚本ID,这里可以指定其他脚本的ID,后续的获取详细信息等任务将会由指定的其他脚本完成。为空则默认为当前脚本 885 | } 886 | ``` 887 | ### EpInfo 888 | ```lua 889 | { 890 | ["name"]=string, --分集名称 891 | ["index"]=number, --分集编号(索引) 892 | ["type"]=number --分集类型 893 | } 894 | ``` 895 | 分集类型包括 EP, SP, OP, ED, Trailer, MAD, Other 七种,分别用1-7表示,默认情况下为1(即EP,本篇) 896 | 897 | 注意,KikoPlay通过分集类型和分集索引两个字段标识一个动画下的分集,如果脚本返回的分集列表中有重复,KikoPlay会自动重新编号 898 | 899 | ### Character 900 | ```lua 901 | { 902 | ["name"]=string, --人物名称 903 | ["actor"]=string, --演员名称 904 | ["link"]=string, --人物资料页URL 905 | ["imgurl"]=string --人物图片URL 906 | } 907 | ``` 908 | ### Anime 909 | ```lua 910 | { 911 | ["name"]=string, --动画名称 912 | ["data"]=string, --脚本可以自行存放一些数据 913 | ["url"]=string, --条目页面URL 914 | ["desc"]=string, --描述 915 | ["airdate"]=string, --放送日期,格式为yyyy-mm-dd 916 | ["epcount"]=number, --分集数 917 | ["coverurl"]=string, --封面图URL 918 | ["staff"]=string, --staff 919 | ["crt"]=Array[Character], --人物 920 | ["scriptId"]=string --脚本ID 921 | } 922 | ``` 923 | 脚本传递到KikoPlay时,staff的格式为:job1:staff1;job2:staff2;.... 924 | 925 | KikoPlay传递到脚本时,staff的格式为: 926 | 927 | ```lua 928 | { 929 | ["job1"]=staff1, 930 | ["job2"]=staff2, 931 | ...... 932 | } 933 | ``` 934 | 935 | 提供封面图URL和人物的图片URL后,KikoPlay会自动从相应URL下载图片。如果不需要KikoPlay下载,URL可以是本地文件路径,但此时KikoPlay不会在数据库中保存URL 936 | 937 | ### MatchResult 938 | ```lua 939 | { 940 | ["success"]=bool, --是否成功关联 941 | ["anime"]=AnimeLite, --关联的动画信息 942 | ["ep"]=EpInfo --关联的剧集信息 943 | } 944 | ``` 945 | ### LibraryMenu 946 | ```lua 947 | { 948 | ["title"]=string, --菜单标题 949 | ["id"]=string --菜单ID 950 | } 951 | ``` 952 | ### ResourceItem 953 | ```lua 954 | { 955 | ["title"]=string, --标题 956 | ["size"]=string, --大小,建议是xxx(GB/MB/KB)形式 957 | ["time"]=string, --发布时间 958 | ["magnet"]=string, --磁力链接(或者其他aria2支持的链接) 959 | ["url"]=string --资源页面 960 | } 961 | ``` 962 | ### NetworkReply 963 | ```lua 964 | { 965 | ["statusCode"]=number, --http状态 966 | ["hasError"]=bool, --是否出错 967 | ["errInfo"]=string, --错误信息 968 | ["content"]=string, --响应内容 969 | ["headers"]={ --响应头 970 | [key]=value, 971 | .... 972 | } 973 | } 974 | ``` 975 | ### BgmSeason 976 | ```lua 977 | { 978 | ["title"]=string, --分季标题 979 | ["data"]=string --脚本可以自行存放一些数据 980 | } 981 | ``` 982 | ### BgmItem 983 | ```lua 984 | { 985 | ["title"]=string, --分季标题 986 | ["weekday"]=number, --放送星期,取值0(星期日)~6(星期六) 987 | ["time"]=string, --放送时间 988 | ["date"]=string, --放送日期 989 | ["isnew"]=bool, --是否新番 990 | ["bgmid"]=string, --bangumi id 991 | ["sites"]=Array[{ 992 | ["name"]=string, 993 | ["url"]=string 994 | },...], --放送站点列表 995 | ["focus"]=bool --用户是否关注 996 | } 997 | ``` 998 | ### ScriptInfo 999 | ```lua 1000 | { 1001 | ["type"]=number, --脚本类型,0:弹幕 1:资料库 2:资源 3:番组日历 1002 | ["id"]=string, --脚本id 1003 | ["name"]=string, --脚本名称 1004 | ["version"]=string, --脚本版本 1005 | ["desc"]=string, --脚本描述信息 1006 | ["path"]=string, --脚本文件路径 1007 | ["min_kiko"]=string, --脚本要求最低KikoPlay版本 1008 | } 1009 | ``` -------------------------------------------------------------------------------- /resource/tpbsource.lua: -------------------------------------------------------------------------------- 1 | -- TPB source 2 | ---------------- 3 | -- 公共部分 4 | -- 脚本信息 5 | info = { 6 | ["name"] = "TPBsrc", 7 | ["id"] = "Kikyou.r.TPBsource", 8 | ["desc"] = "TPBsource 资源信息脚本(测试中,不稳定) Edited by: anonymous\n"..-- Edited by: anonymous 9 | "从 thePirateBay 刮削媒体资源信息。", 10 | ["version"] = "0.0.05", -- 0.0.05.230325_build 11 | ["min_kiko"] = "0.9.1", 12 | } 13 | 14 | -- 设置项 15 | settings = { 16 | ["aname_website_alias"] = { 17 | ["title"] = "网址 - TPB域名", 18 | ["default"] = "tpb.party", 19 | ["desc"] = "[必填项] 填写 `thePirateBay` 的域名,通常请不要带`https://`、`/`等字符,\n".. 20 | "适用于其搜索页网址形如 [https://域名/search/搜索关键词/页码/排序/类别] 的。\n".. 21 | "`tpb.party`:网址 [https://tpb.party/search/搜索关键词/1/3/0] 为其搜索页 (默认)。", 22 | ["group"]="网址", 23 | }, 24 | -- ["aname_website_alias2"] = { 25 | -- ["title"] = "网址 - TPB域名格式2", 26 | -- ["default"] = "thepiratebaya.org", 27 | -- ["desc"] = "[选填项] (此设置不可用) 填写 `thePirateBay` 的域名,通常请不要带`https://`、`/`等字符,\n".. 28 | -- "适用于其搜索页网址形如 [https://域名/search?q=搜索关键词] 的。\n".. 29 | -- "注意:只有`网址 - TPB域名`不填写时才会采用此设置项。\n".. 30 | -- "`thepiratebaya.org`:网址 [https://thepiratebaya.org/search?q=搜索关键词] 为其搜索页 (默认)。", 31 | -- ["group"]="网址", 32 | -- }, 33 | ["list_filter_category"] = { 34 | ["title"] = "限定 - 资源所属类别", 35 | ["default"] = "0", 36 | ["desc"] = "限定资源列表搜索 只属于此类别的资源。 `0`:所有(all/x/) (默认)。\n".. 37 | "`100` 音频(audio):`101` 音乐、`102` 有声书、`103` 声音片段、`104` 无损音频、`199` 其他音频。\n".. 38 | "`200` 视频([v]ideo):`201` 电影、`202` 电影(DVD压制)、`203` MV/音乐视频(mv)、`204` 电影片段、`205` 剧集、`206` 手持设备、`207` 电影(高清)、`208` 剧集(高清)、`209` 3D视频(3d)、`299` 其他视频。\n".. 39 | "`300` 应用([app]lication):`301` Windows、`302` Mac、`303` UNIX、`304` 手持设备、`305` IOS (iPad/iPhone)、`306` Android、`399` 其他系统下的应用。\n".. 40 | "`400` 游戏([g]ame):`401` PC、`402` Mac、`403` PSx、`404` XBOX360、`405` Wii、`406` 手持设备、`407` IOS (iPad/iPhone)、`408` Android、`499` 其他游戏。\n".. -- `500` 淫秽:`501` 电影、`502` 电影(DVD压制)、`503` 图像、`504` 游戏、`505` 电影(高清)、`506` 电影片段、`599` 其他。\n" 41 | "`600` 其他(other(s)):`601` 电子书、`602` 漫画(comic(s))、`603` 图像、`604` 封面、`605` 3D打印模型、`699` 其他。", 42 | ["choices"]="0,100,101,102,103,104,199,200,201,202,203,204,205,206,207,208,209,299".. 43 | ",300,301,302,303,304,305,306,399,400,401,402,403,404,405,406,407,408,499".. 44 | ",500,501,502,503,504,505,506,599,600,601,602,603,604,605,699", -- 45 | ["group"]="限定", 46 | }, 47 | ["list_filter_orderby"] = { 48 | ["title"] = "限定 - 资源排序方式", 49 | ["default"] = "7", 50 | ["desc"] = "限定资源列表搜索按此排序资源。 降序:从大到小(desc/-/);升序:从小到大(asc/+) `7`:做种节点数 由多到少 (默认)。\n".. 51 | "`1`/`2`:按 标题(name) 降序ZA/升序。\t`13`/`14`:按 类型(category) 降序/升序。 \n".. 52 | "`3`/`4`:按 上传时间([t]ime) 降序新旧/升序。\t`5`/`6`:按 文件大小(si[z]e) 降序/升序。 \n".. 53 | "`7`/`8`:按 做种节点数([s]eeder(s)/) 降序/升序。 `9`/`10`:按 吸血节点数(leecher(s)) 降序/升序。 ", -- "`11`/`12`:按 上传者昵称(uploader) 降序/升序。 \n".. 54 | ["choices"]="1,2,3,4,5,6,7,8,9,10,11,12,13,14", 55 | ["group"]="限定", 56 | }, 57 | ["list_filter_qtext"] = { 58 | ["title"] = "限定 - 关键词限定", 59 | ["default"] = "filter", 60 | ["desc"] = "在搜索的关键词后加上形如 `$filter=排序编号/类别编号$` 的文本来限定资源列表搜索,即以字符'$'分隔关键词和限定词\n".. 61 | "例如输入 `HEVC Bluray $filter=3/200`,即为 搜索`x265 Bluray`的视频、列表按上传时间由新到旧排列。\n".. 62 | "以下为与上例`$filter=3/200`同义的限定词: `$filter=time_desc/video`、`$filter:t-/v`等。\n".. 63 | "`plain`:所有输入仅作为普通关键词。 `filter`:识别形如`$filter=0/7$`的限定 (默认)。 ", -- "`11`/`12`:按 上传者昵称 降序/升序。 \n".. 64 | ["choices"]="plain,filter", 65 | ["group"]="限定", 66 | }, 67 | } 68 | 69 | scriptmenus = { 70 | {["title"]="检测连接", ["id"]="detect_valid_connect"}, 71 | -- {["title"]="使用方法", ["id"]="link_repo_usage"}, 72 | {["title"]="关于", ["id"]="display_dialog_about"}, 73 | } 74 | 75 | searchsettings = { 76 | ["list_filter_i07_qtext"] = { 77 | ["title"] = "限定:", 78 | -- ["default"] = "filter", 79 | ["desc"] = "在搜索的关键词后加上形如 `$filter=排序编号/类别编号$` 的文本来限定资源列表搜索,即以字符'$'分隔关键词和限定词\n".. 80 | "例如输入 `HEVC Bluray $filter=3/200`,即为 搜索`x265 Bluray`的视频、列表按上传时间由新到旧排列。\n".. 81 | "以下为与上例`$filter=3/200`同义的限定词: `$filter=time_desc/video`、`$filter:t-/v`等。\n".. 82 | "识别形如`$filter=0/7$`的限定 (`限定 - 关键词限定`的默认设置)。 ", -- "`11`/`12`:按 上传者昵称 降序/升序。 \n".. 83 | -- ["choices"]="plain,filter", 84 | ["save"]=false, 85 | ["display_type"] = 5, 86 | }, 87 | ["list_filter_i23_orderby"] = { 88 | ["title"] = "排序", 89 | ["default"] = "默认", 90 | ["desc"] = "限定资源列表搜索按此排序资源。 降序:从大到小(desc/-/);升序:从小到大(asc/+) `7`:做种节点数 由多到少。\n".. 91 | "`1`/`2`:按 标题(name) 降序ZA/升序。\t`13`/`14`:按 类型(category) 降序/升序。 \n".. 92 | "`3`/`4`:按 上传时间([t]ime) 降序新旧/升序。\t`5`/`6`:按 文件大小(si[z]e) 降序/升序。 \n".. 93 | "`7`/`8`:按 做种节点数([s]eeder(s)/) 降序/升序。 `9`/`10`:按 吸血节点数(leecher(s)) 降序/升序。 ", -- "`11`/`12`:按 上传者昵称(uploader) 降序/升序。 \n".. 94 | ["choices"]="默认,标题降序,标题升序,时间降序,时间升序,大小降序,大小升序,做种数降序,做种数升序,吸血数降序,吸血数升序,上传者降序,上传者升序,类别降序,类别升序", 95 | ["save"]=true, 96 | ["display_type"] = 1, 97 | }, 98 | ["list_filter_i33_category"] = { 99 | ["title"] = "类别", 100 | ["default"] = "默认", 101 | ["desc"] = "限定资源列表搜索 只属于此类别的资源。 `0`:所有(all/x/)。\n".. 102 | "`100` 音频(audio):`101` 音乐、`102` 有声书、`103` 声音片段、`104` 无损音频、`199` 其他音频。\n".. 103 | "`200` 视频([v]ideo):`201` 电影、`202` 电影(DVD压制)、`203` MV/音乐视频(mv)、`204` 电影片段、`205` 剧集、`206` 手持设备、`207` 电影(高清)、`208` 剧集(高清)、`209` 3D视频(3d)、`299` 其他视频。\n".. 104 | "`300` 应用([app]lication):`301` Windows、`302` Mac、`303` UNIX、`304` 手持设备、`305` IOS (iPad/iPhone)、`306` Android、`399` 其他系统下的应用。\n".. 105 | "`400` 游戏([g]ame):`401` PC、`402` Mac、`403` PSx、`404` XBOX360、`405` Wii、`406` 手持设备、`407` IOS (iPad/iPhone)、`408` Android、`499` 其他游戏。\n".. -- `500` 淫秽:`501` 电影、`502` 电影(DVD压制)、`503` 图像、`504` 游戏、`505` 电影(高清)、`506` 电影片段、`599` 其他。\n" 106 | "`600` 其他(other(s)):`601` 电子书、`602` 漫画(comic(s))、`603` 图像、`604` 封面、`605` 3D打印模型、`699` 其他。", 107 | ["choices"]="默认,所有,音频,视频,应用,游戏,视频MV,视频3D,漫画,电子书,0,100,101,102,103,104,199,200,201,202,203,204,205,206,207,208,209,299".. 108 | ",300,301,302,303,304,305,306,399,400,401,402,403,404,405,406,407,408,499".. 109 | ",500,501,502,503,504,505,506,599,600,601,602,603,604,605,699", -- 110 | ["save"]=true, 111 | ["display_type"] = 1, 112 | }, 113 | } 114 | 115 | Filter_info ={ 116 | ["order"]={ ["name"]="1", ["time"]="3", ["size"]="5", ["uploader"]="11", ["category"]="13", 117 | ["title"]="1", ["seeders"]="7", ["leechers"]="9", ["seeder"]="7", ["leecher"]="9", 118 | ["标题"]="1", ["时间"]="3", ["大小"]="5", ["做种节点"]="7", ["吸血节点"]="9", ["上传者"]="11", ["目录"]="13", 119 | [""]="7", ["t"]="3", ["z"]="5", ["s"]="7", 120 | }, 121 | ["category"]={ ["all"]="0", ["audio"]="100", ["video"]="200", ["application"]="300", ["game"]="400", 122 | ["audios"]="100", ["videos"]="200", ["applications"]="300", ["games"]="400", ["others"]="600", ["adults"]="500", 123 | ["app"]="300", ["apps"]="300", ["other"]="600", ["adult"]="500", ["porn"]="500", ["porns"]="500", 124 | [""]="0", ["x"]="0", ["v"]="200", ["g"]="400", ["mv"]="203", ["3D"]="209", ["comics"]="602", ["comic"]="602", 125 | ["所有"]="0", ["音频"]="100", ["视频"]="200", ["应用"]="300", ["游戏"]="400", ["成人"]="500", ["视频MV"]="203", ["视频3D"]="209", ["漫画"]="602", ["电子书"]="601", 126 | }, 127 | ["ascdesc"]={ ["asc"]="1", ["desc"]="0", [""]="0", ["+"]="1", ["-"]="0", ["升序"]="1", ["降序"]="0", 128 | }, 129 | ["orderby"]={["标题降序"]="1", ["时间降序"]="3", ["大小降序"]="5", ["做种数降序"]="7", ["吸血数降序"]="9", ["上传者降序"]="11", ["类别降序"]="13", 130 | ["标题升序"]="2", ["时间升序"]="4", ["大小升序"]="6", ["做种数升序"]="8", ["吸血数升序"]="10", ["上传者升序"]="12", ["类别升序"]="14", 131 | }, 132 | } 133 | Datetime={} 134 | 135 | -- (() and{} or{})[1] 136 | 137 | --------------------- 138 | -- 资源脚本部分 139 | -- copy (as template) from & thanks to "../resource/comicat.lua" in "KikoPlay/resource"|KikoPlayScript 140 | -- 141 | 142 | function search(keyword,page,scene,options) 143 | --kiko_HttpGet arg: 144 | -- url: string 145 | -- query: table, {["key"]=value} value: string 146 | -- header: table, {["key"]=value} value: string 147 | page= math.floor(tonumber(page) or 1) 148 | if page <1 then page= 1 end 149 | 150 | local orderFp = table.deepCopy(Filter_info.order) 151 | for ofk,ofv in pairs(Filter_info.order) do 152 | orderFp[ofk.."-"]= ofv 153 | orderFp[ofk.."+"]= tostring(math.floor((tonumber(ofv) or 7) +1)) 154 | end 155 | 156 | local keywordPlain,listFilter = nil,nil 157 | local slf_category,slf_orderby,slf_qtext = nil,nil,nil 158 | if scene == "auto-download" then 159 | slf_category = settings["list_filter_category"] 160 | slf_orderby = settings["list_filter_orderby"] 161 | slf_qtext =settings["list_filter_qtext"] 162 | elseif true or scene == "search" then 163 | slf_category = ((string.isEmpty(options["list_filter_i33_category"]) or options["list_filter_i33_category"]=="默认") 164 | and{settings["list_filter_category"]} or{Filter_info.category[options["list_filter_i33_category"]] or options["list_filter_i33_category"]})[1] 165 | slf_orderby = ((string.isEmpty(options["list_filter_i23_orderby"]) or options["list_filter_i23_orderby"]=="默认") 166 | and{settings["list_filter_orderby"]} or{Filter_info.orderby[options["list_filter_i23_orderby"]] or options["list_filter_i23_orderby"]})[1] 167 | slf_qtext = ((string.isEmpty(options["list_filter_i07_qtext"]) or options["list_filter_qtext"]=="默认") 168 | and{settings["list_filter_qtext"]} or{options["list_filter_i07_qtext"]})[1] 169 | end 170 | if slf_qtext=="plain" then 171 | keywordPlain= string.trim(keyword) 172 | listFilter= slf_orderby .."/"..slf_category 173 | elseif true or slf_qtext=="filter" then 174 | local keywordT= string.split(keyword,"$") 175 | for _,ktV in ipairs(keywordT) do 176 | local ktVp= string.gsub(ktV, "%s+","") 177 | local ktvFil,ktvFir = string.find(ktVp, "f[ilter]*[:=]") 178 | local ktvOv,ktvCv = nil,nil 179 | local isSuccess=false 180 | if ktvFil~=nil and string.isEmpty(listFilter) then 181 | local lfSettings={ 182 | ["category"]= slf_category, 183 | ["orderby"]= slf_orderby, 184 | ["order"]= math.floor(tonumber(slf_orderby) - (tonumber(slf_orderby) -1) %2), 185 | ["ascdesc"]= math.floor((tonumber(slf_orderby) -1) %2), 186 | } 187 | local ktvOir = nil 188 | _,ktvOir,ktvOv = string.find(ktVp, "([%d%a_%+%-]+)[/\\]?",ktvFir) 189 | ktvOv= string.gsub(ktvOv or "","[/\\]","") 190 | for fioK,fioV in pairs(orderFp) do 191 | if string.isEmpty(ktvOv) then 192 | kiko.log("[WARN] TPB.Search-Keyword.filter-orderby: Not found.") 193 | kiko.message("[警告] 无法找到 顺序限定。",1|8) 194 | ktvOv= lfSettings.order 195 | isSuccess= true 196 | break; 197 | elseif tonumber(ktvOv)~=nil then 198 | if ktvOv==fioV then 199 | isSuccess= true 200 | break; 201 | end 202 | else 203 | local ktvOvs= string.split(ktvOv or "","_") 204 | if (ktvOvs[1] or "")==fioK or ktvOvs[1]==fioV then 205 | if string.sub(ktvOvs[1],#(ktvOvs[1]))=="-" or string.sub(ktvOvs[1],#(ktvOvs[1]))=="+" then 206 | isSuccess= true 207 | ktvOv= fioV 208 | break; 209 | end 210 | ktvOvs[1]= tonumber(fioV) 211 | end 212 | if type(ktvOvs[1])=="number" then 213 | if string.isEmpty(ktvOvs[2]) then 214 | ktvOvs[2]= lfSettings.ascdesc 215 | isSuccess= true 216 | else 217 | for fioaK,fioaV in pairs(Filter_info.ascdesc) do 218 | if (ktvOvs[2] or "")==fioaK or ktvOvs[2]==fioaV then 219 | ktvOvs[2]= tonumber(fioaV) 220 | isSuccess= true 221 | break; 222 | end 223 | end 224 | end 225 | end 226 | if isSuccess then 227 | ktvOv= tostring(math.floor(ktvOvs[1] + ktvOvs[2])) 228 | break; 229 | end 230 | end 231 | end 232 | if not isSuccess then 233 | kiko.log("[WARN] TPB.Search-Keyword.filter-orderby: Invalid.") 234 | kiko.message("[警告] 识别到 排序限定 无效。",1|8) 235 | ktvOv= lfSettings.order 236 | isSuccess= true 237 | end 238 | _,_,ktvCv = string.find(ktVp, "([%d%a]+)",ktvOir or ktvFir) 239 | isSuccess= false 240 | if string.isEmpty(ktvCv) then 241 | kiko.log("[WARN] TPB.Search-Keyword.filter-category: Not found.") 242 | kiko.message("[警告] 无法找到 类别限定。",1|8) 243 | ktvCv= lfSettings.category 244 | isSuccess= true 245 | else 246 | for ficK,ficV in pairs(Filter_info.category) do 247 | if tonumber(ktvCv)~=nil then 248 | if ktvCv==ficV then 249 | isSuccess= true 250 | break; 251 | end 252 | elseif (ktvCv or "")==ficK or ktvCv==ficV then 253 | ktvCv=ficV 254 | isSuccess= true 255 | break; 256 | end 257 | end 258 | end 259 | if not isSuccess then 260 | kiko.log("[WARN] TPB.Search-Keyword.filter-category: Invalid.") 261 | kiko.message("[警告] 识别到 类别限定 无效。",1|8) 262 | ktvCv= lfSettings.category 263 | isSuccess= true 264 | end 265 | end 266 | if isSuccess then 267 | listFilter= ktvOv .."/".. ktvCv 268 | elseif ktvFil~=nil and string.isEmpty(listFilter) then 269 | kiko.log("[WARN] TPB.Search-Keyword.filter-syntax: Wrong syntax.") 270 | kiko.message("[警告] 识别到限定语句 错误。",1|8) 271 | elseif ktvFil~=nil and not string.isEmpty(listFilter) then 272 | kiko.log("[WARN] TPB.Search-Keyword.filter-syntax: Too many syntax(es).") 273 | kiko.message("[警告] 识别到限定语句 重复。",1|8) 274 | elseif true then 275 | keywordPlain= (string.isEmpty(keywordPlain) and{""} or{keywordPlain .." "})[1] .. string.trim(ktV) 276 | end 277 | end 278 | keywordPlain= (string.isEmpty(keywordPlain) and{""} or{keywordPlain})[1] 279 | listFilter= (string.isEmpty(listFilter) and{slf_orderby 280 | .."/"..slf_category} or{listFilter})[1] 281 | end 282 | kiko.log("[INFO] TPB: searching <"..keywordPlain.."> in filter <"..listFilter..">.") 283 | 284 | local err, reply=kiko.httpget("http://".. settings["aname_website_alias"] .."/search/".. 285 | string.gsub(keywordPlain, "[ %c%p%^%&%|<>]", "%%20") .."/".. page .."/".. 286 | (listFilter or "7/0")) 287 | if err~=nil or (reply or{}).hasError==true then 288 | kiko.log("[ERROR] TPB.reply-search.httpget: ".. (err or "").."<".. 289 | string.format("%03d",(reply or{}).statusCode or 0)..">"..((reply or{}).errInfo or"")) 290 | error((err or "").."<".. string.format("%03d",(reply or{}).statusCode or 0)..">"..((reply or{}).errInfo or"")) 291 | end 292 | local content = reply["content"] 293 | 294 | local os_time = os.time() 295 | local _,_,pageLx,pageRx,totalCount=string.find(content,"Search results:%s*[^<]* Displaying hits from (%d+) to (%d+) %(approx (%d+) found%)") 296 | if pageLx==nil or pageRx==nil or totalCount==nil then 297 | error("[ERROR] TPB.webPage.decode: None of page index / total count is found.") 298 | end 299 | if totalCount=="0" or pageLx==pageRx then 300 | return {}, 0 301 | end 302 | -- pageLx= math.floor(tonumber(pageLx) or 0) 303 | -- pageRx= math.floor(tonumber(pageRx) or 0) 304 | -- totalCount= math.floor(tonumber(totalCount) or 0) 305 | local pageCount=math.ceil(tonumber(totalCount) / (tonumber(pageRx) - tonumber(pageLx))) 306 | kiko.log("[INFO] ".. (totalCount or "0") .." items on <".. keyword .."> are found, showing index ".. pageLx .."~".. pageRx ..".") 307 | 308 | local patternF,kregexF = nil,nil 309 | patternF=[[(?<=)\s*([\s\S]+)\s*(?=)]] 310 | kregexF = kiko.regex(patternF,"i") 311 | local spos,epos,_= kregexF:find(content) 312 | if spos==nil then 313 | error("[ERROR] TPB.webPage.decode: None of table rows is found.") 314 | end 315 | local npos=spos -- 每个循环块的结束index 为下一次开始index 316 | local itemsList={} 317 | while npos\s*([^"]*)]] 321 | kregexF = kiko.regex(patternF,"i") 322 | _,lnpos,url,title= kregexF:find(content,lnpos) 323 | if lnpos==nil then break end 324 | patternF= [[Uploaded ([^,]*), Size ([^,]*), ULed by\s*<[^>]*>([^<]*) ]] 328 | kregexF = kiko.regex(patternF,"i") 329 | _,lnpos,time,size,uploader= kregexF:find(content,lnpos) 330 | title= string.unescape(title) 331 | size= string.gsub(size or "", "(%d*%.?%d*) ([IiBbKkMmGgTtPp]*)", "%1 %2") -- 667.49 MiB 332 | time= string.gsub(time or "", " ", " ") 333 | time= string.gsub(time or "", "", "") 334 | time= string.gsub(time or "", "", "") 335 | time= string.gsub(time or "", "(%d*)%-(%d*) (%d*)$", "%3-%1-%2") -- 01-06 2020 336 | local thour,tmin = nil,nil 337 | thour,tmin = string.match(time or ""," (%d*):(%d*)$") 338 | if (tonumber(thour) ~= nil and tonumber(thour) ~= nil) then 339 | local tdays,tmins,tz_total_min,sdday = nil,nil,nil,nil,nil 340 | tz_total_min = math.floor(tonumber(string.sub(os.date("%z",os_time) or"",1,3)) or 0) *60 + 341 | math.floor(tonumber(string.sub(os.date("%z",os_time) or"",4,5)) or 0) 342 | - 1 * 60 -- TODO: TPB并非世界时的时刻,相差1h,原因未知 343 | 344 | local t_ymd= string.gsub(time or "", "^(%d*)%-(%d*) ", os.date("%Y",os_time - tz_total_min *60).."-%1-%2 ") -- 01-06 20:20 345 | if(t_ymd~=nil) then 346 | local t_stamp = Datetime.strToStamp(string.format("%sT%s:00Z%s",t_ymd, string.match(time, "%d*:%d*"), 347 | string.format("%+03d:%02d",math.floor(tz_total_min/60),math.floor(tz_total_min%60)))) 348 | time= string.gsub(time or "", "^%d*%-%d* %d*:%d*", os.date("%Y-%m-%d %H:%M",t_stamp)) -- Today 02:59 349 | 350 | end 351 | tmins = math.floor(tonumber(thour)) *60 + math.floor(tonumber(tmin)) + tz_total_min 352 | thour = math.floor(tmins/60%24) 353 | tmin = math.floor(tmins%60) 354 | -- tday = math.floor(tmins/60/24) 355 | tdays = nil 356 | tdays = ((string.match(time or "","Y%-day")) and{-1} or{tdays})[1] 357 | tdays = ((string.match(time or "","Today")) and{0} or{tdays})[1] 358 | if(tdays ~= nil) then 359 | sdday = os.date("%Y-%m-%d",os_time - tz_total_min *60 + tdays *3600*24) 360 | time= string.gsub(time or "", "Y%-day %d*:%d*", string.format("%s %02d:%02d", sdday, thour, tmin)) -- Y-day 02:59 361 | time= string.gsub(time or "", "Today %d*:%d*", string.format("%s %02d:%02d", sdday, thour, tmin)) -- Today 02:59 362 | end 363 | end 364 | local n_minsago,sdt_minsago = tonumber(string.match(time or "", "(%d*) mins* ago")), "" 365 | if n_minsago ~= nil then 366 | sdt_minsago = os.date("%Y-%m-%d %H:%M",os_time - n_minsago *60) 367 | end 368 | time= string.gsub(time or "", "(%d*) mins* ago", sdt_minsago) -- 20 mins ago 369 | 370 | table.insert(itemsList,{ 371 | ["title"]=title, 372 | ["size"]=size, 373 | ["time"]=time, 374 | ["magnet"]=magnet, 375 | ["url"]=url 376 | }) 377 | _,lnpos=string.find(content,""..((reply or{}).errInfo or"")) 397 | kiko.dialog({ 398 | ["title"]="测试 thePirateBay 的域名是否有效连接", 399 | ["tip"]="[错误]\t"..(err or "").."<".. string.format("%03d",(reply or{}).statusCode or 0).. 400 | "> "..((reply or{}).errInfo or"").."!", 401 | ["text"]="+ thePirateBay 网站代理/镜像的合集(可能不安全) - https://piratebay-proxylist.com", 402 | }) 403 | -- error((err or "").."<".. string.format("%03d",(reply or{}).statusCode)..">"..(reply or{}).errInfo or"") 404 | else 405 | kiko.dialog({ 406 | ["title"]="测试 thePirateBay 的域名是否有效连接", 407 | ["tip"]="\t成功设置 `网址 - TPB域名` !", 408 | ["text"]=nil, 409 | }) 410 | end 411 | end 412 | end 413 | 414 | function scriptmenuclick(menuid) 415 | if menuid == "detect_valid_connect" then 416 | 417 | local diaTitle, diaTip, diaText = "检测 - 域名是否有效连接","","" 418 | local hg_theme= "Fleabug/1" -- Flebag (2016) 419 | local err, reply=kiko.httpget("http://".. settings["aname_website_alias"] .."/search/".. hg_theme 420 | .."/"..settings["list_filter_orderby"] .."/"..settings["list_filter_category"]) 421 | if err~=nil or (reply or{}).hasError==true then 422 | kiko.log("[ERROR] TPB.reply-test.httpget: ".. (err or "").."<".. 423 | string.format("%03d",(reply or{}).statusCode or 0)..">"..((reply or{}).errInfo or"")) 424 | diaTip= "[错误]\t"..(err or "").."<".. string.format("%03d",(reply or{}).statusCode or 0).. 425 | "> "..((reply or{}).errInfo or"").."!" 426 | diaText= "+ thePirateBay 网站代理/镜像的合集(可能不安全) - https://piratebay-proxylist.com" 427 | -- error((err or "").."<".. string.format("%03d",(reply or{}).statusCode)..">"..(reply or{}).errInfo or"") 428 | else 429 | diaTip= "\t成功连接 `网址 - TPB域名` !" 430 | end 431 | 432 | kiko.dialog({ 433 | ["title"]= diaTitle, 434 | ["tip"]= diaTip, 435 | ["text"]= (string.isEmpty(diaText) and{nil} or{diaText})[1], 436 | }) 437 | -- elseif menuid == "link_repo_usage" then 438 | -- kiko.execute(true, "cmd", {"/c", "start", "https://github.com/---"}) 439 | elseif menuid == "display_dialog_about" then 440 | local img_back_data= nil 441 | -- local header = {["Accept"] = "image/jpeg" } 442 | -- local err, reply = kiko.httpget("https://github.com/---", {} , header) 443 | -- if err ~= nil then 444 | -- img_back_data=nil 445 | -- else 446 | -- img_back_data=reply["content"] 447 | -- end 448 | kiko.dialog({ 449 | ["title"]= "关于", 450 | ["tip"]= "\t\t\t\tEdited by: anonymous\n\n".. 451 | "脚本 TPBsource (/bgm_calendar/traktlist.lua) 是一个媒体资源信息脚本,\n".. 452 | "主要从 thePirateBay 刮削媒体资源信息。\n".. 453 | "\n欢迎到 KikoPlay的QQ群 反馈!\n",-- 此脚本的GitHub页面 或 454 | ["text"]= -- "+ 此脚本的GitHub页面 - \n".. 455 | -- "\t 用法、常见问题…\n".. 456 | "\n本脚本基于:\n".. 457 | "+ thePirateBay 网站\n".. 458 | "+ thePirateBay 网站代理/镜像的合集(可能不安全) - https://piratebay-proxylist.com\n".. 459 | "+ 其他另见脚本内注释\n".. 460 | "\nKikoPlay:\n".. 461 | "+ KikoPlay 首页 - https://kikoplay.fun/\n".. 462 | "+ KikoPlay的GitHub页面 - https://github.com/KikoPlayProject/KikoPlayScript\n".. 463 | "+ KikoPlay 脚本仓库 - https://github.com/KikoPlayProject/KikoPlayScript", 464 | ["image"]= img_back_data, 465 | }) 466 | end 467 | end 468 | 469 | --------------------- 470 | -- 功能函数 471 | -- 472 | 473 | -- copy from & thanks to "../resource/mikan.lua" in "KikoPlay/resource"|KikoPlayScript 474 | -- 替换`&..`转义字符 475 | function string.unescape(str) 476 | if type(str) ~= "string" then return str end 477 | str = string.gsub( str, '<', '<' ) 478 | str = string.gsub( str, '>', '>' ) 479 | str = string.gsub( str, '"', '"' ) 480 | str = string.gsub( str, ''', "'" ) 481 | str = string.gsub( str, '&#(%d+);', function(n) return utf8.char(n) end ) 482 | str = string.gsub( str, '&#x(%x+);', function(n) return utf8.char(tonumber(n,16)) end ) 483 | str = string.gsub( str, '&', '&' ) -- Be sure to do this after all others 484 | return str 485 | end 486 | 487 | -- copy from & thanks to - https://blog.csdn.net/fightsyj/article/details/85057634 488 | --* string.split("abc","b") 489 | --* @return: (table){} - 无匹配,返回 (table){input} 490 | function string.split(input, delimiter) 491 | -- 分隔符nil,返回 (table){input} 492 | if type(delimiter) == nil then 493 | return {input} 494 | end 495 | -- 转换为string类型 496 | input = tostring(input) 497 | delimiter = tostring(delimiter) 498 | -- 分隔符空字符串,返回 (table){input} 499 | if (delimiter == "") then 500 | return {input} 501 | end 502 | 503 | -- 坐标;分割input后的(table) 504 | local pos, arr = 0, {} 505 | -- 从坐标每string.find()到一个匹配的分隔符,获取起止坐标 506 | for st, sp in function() return string.find(input, delimiter, pos, true) end do 507 | -- 插入 旧坐标到 分隔符开始坐标-1 的字符串 508 | table.insert(arr, string.sub(input, pos, st - 1)) 509 | -- 更新坐标为 分隔符结束坐标+1 510 | pos = sp + 1 511 | end 512 | -- 插入剩余的字符串 513 | table.insert(arr, string.sub(input, pos)) 514 | return arr 515 | end 516 | -- string.isEmpty():: nil->true | "" -> true 517 | function string.isEmpty(input) 518 | if input==nil or tostring(input)=="" 519 | or not (type(input)=="string" or type(input)=="number" or type(input)=="boolean") then 520 | return true 521 | else return false 522 | end 523 | end 524 | -- string.trim():: nil->nil | ""->"" 525 | function string.trim(input) 526 | if type(input)~="string" then 527 | return input 528 | end 529 | input= string.gsub(input,"^%s+","") 530 | input= string.gsub(input,"%s+$","") 531 | return input 532 | end 533 | 534 | -- 深拷贝,包含元表(?),不考虑键key为
    的情形 535 | -- copy from & thanks to - https://blog.csdn.net/qq_36383623/article/details/104708468 536 | function table.deepCopy(tb) 537 | if tb == nil then 538 | return {} 539 | end 540 | if type(tb) ~= "table" then 541 | -- 排除非
    的变量 542 | return nil 543 | end 544 | local copy = {} 545 | for k, v in pairs(tb) do 546 | if type(v) == 'table' then 547 | -- 值是
    ,递归复制
    值 548 | copy[k] = table.deepCopy(v) 549 | else 550 | -- 普通值,直接赋值 551 | copy[k] = v 552 | end 553 | end 554 | -- local meta = table.deepCopy(getmetatable(tb)) 555 | -- 设置元表。 556 | setmetatable(copy, table.deepCopy(getmetatable(tb)) or{}) 557 | return copy 558 | end 559 | 560 | --* (string)str :: "2022-03-01T07:00:00-05:00" -> timestamp 561 | function Datetime.strToStamp(input) 562 | local dateTa={} 563 | local secZdt=0 564 | -- if isFromLocal==nil then isFromLocal=false end 565 | 566 | local p_stamp = os.time() 567 | local timezone = (math.floor(tonumber(string.sub(os.date("%z",os.time()) or"",1,3)) or 0)) *3600 + 568 | (math.floor(tonumber(string.sub(os.date("%z",os.time()) or"",4,5)) or 0)) *60 569 | if string.isEmpty(input) then 570 | return p_stamp 571 | else 572 | local dataTap= os.date("*t",p_stamp) 573 | dateTa={ 574 | ["year"]= math.floor(tonumber(string.sub(input,1,4) or"") or tonumber(dataTap.year) or 2022), 575 | ["month"]= math.floor(tonumber(string.sub(input,6,7) or"") or 1), 576 | ["day"]= math.floor(tonumber(string.sub(input,9,10) or"") or 1), 577 | ["hour"]= math.floor(tonumber(string.sub(input,12,13) or"") or 0), 578 | ["min"]= math.floor(tonumber(string.sub(input,15,16) or"") or 0), 579 | ["sec"]= math.floor(tonumber(string.sub(input,18,19) or"") or 0), 580 | } 581 | secZdt= ((tonumber(string.sub(input,20,22) or"") or tonumber(string.sub(input,24,25) or"")) 582 | and{ math.floor(tonumber(string.sub(input,20,22) or"") or 0)*3600 + 583 | math.floor(tonumber(string.sub(input,24,25) or"") or 0)*60 } 584 | or{timezone})[1] 585 | end 586 | return os.time(dateTa) -secZdt +timezone 587 | end 588 | --------------------------------------------------------------------------------