From b8581b4e5e8adc712ef15e9a8a32a80e96681403 Mon Sep 17 00:00:00 2001 From: dyphire Date: Mon, 27 Jan 2025 13:55:49 +0800 Subject: [PATCH] feat: add new api support Brings support for non-animated danmaku --- api.lua | 95 +++++++++------ extra.lua | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++++ main.lua | 81 +++++++----- options.lua | 3 + render.lua | 3 +- 5 files changed, 460 insertions(+), 68 deletions(-) create mode 100644 extra.lua diff --git a/api.lua b/api.lua index 88697a4..e651c63 100644 --- a/api.lua +++ b/api.lua @@ -36,8 +36,6 @@ function url_decode(str) str = str:gsub('%?.+', '') :gsub('%+', ' ') return str - else - return end end @@ -135,6 +133,51 @@ function write_json_file(file_path, data) file:close() end +function write_history(episodeid) + local history = {} + local path = mp.get_property("path") + local dir = get_parent_directory(path) + local fname = mp.get_property('filename/no-ext') + local episodeNumber = 0 + if episodeid then + episodeNumber = tonumber(episodeid) % 1000 + elseif danmaku.extra then + episodeNumber = danmaku.extra.episodenum + end + + if is_protocol(path) then + local title, season_num, episod_num = get_title() + if title and episod_num then + if season_num then + dir = title .." Season".. season_num + else + dir = title + end + fname = url_decode(mp.get_property("media-title")) + episodeNumber = episod_num + end + end + + if dir ~= nil then + local history_json = read_file(history_path) + if history_json ~= nil then + history = utils.parse_json(history_json) or {} + end + history[dir] = {} + history[dir].fname = fname + history[dir].source = danmaku.source + history[dir].animeTitle = danmaku.anime + history[dir].episodeTitle = danmaku.episode + history[dir].episodeNumber = episodeNumber + if episodeid then + history[dir].episodeId = episodeid + elseif danmaku.extra then + history[dir].extra = danmaku.extra + end + write_json_file(history_path, history) + end +end + function itable_index_of(itable, value) for index = 1, #itable do if itable[index] == value then @@ -378,41 +421,10 @@ end -- 读取episodeId获取danmaku function set_episode_id(input, from_menu) from_menu = from_menu or false + danmaku.source = "dandanplay" local episodeId = tonumber(input) if from_menu and options.auto_load or options.autoload_for_url then - local history = {} - local path = mp.get_property("path") - local dir = get_parent_directory(path) - local fname = mp.get_property('filename/no-ext') - local episodeNumber = tonumber(episodeId) % 1000 --动漫的集数 - - if is_protocol(path) then - local title, season_num, episod_num = get_title() - if title and episod_num then - if season_num then - dir = title .." Season".. season_num - else - dir = title - end - fname = url_decode(mp.get_property("media-title")) - episodeNumber = episod_num - end - end - - --将文件名:episodeId写入history.json - if dir ~= nil then - local history_json = read_file(history_path) - if history_json ~= nil then - history = utils.parse_json(history_json) or {} - end - history[dir] = {} - history[dir].animeTitle = danmaku.anime - history[dir].episodeTitle = danmaku.episode - history[dir].episodeId = episodeId - history[dir].episodeNumber = episodeNumber - history[dir].fname = fname - write_json_file(history_path, history) - end + write_history(episodeid) end if options.load_more_danmaku then fetch_danmaku_all(episodeId, from_menu) @@ -951,7 +963,7 @@ function get_danmaku_with_hash(file_name, file_path) end danmaku.anime = match_data.matches[1].animeTitle - danmaku.episode = match_data.matches[1].episodeTitle:match("(第%d+[话回集]+)") or match_data.matches[1].episodeTitle + danmaku.episode = match_data.matches[1].episodeTitle -- 获取并加载弹幕数据 set_episode_id(match_data.matches[1].episodeId, true) @@ -1205,6 +1217,7 @@ function auto_load_danmaku(path, dir, filename, number) local history_number = history_dir.episodeNumber local history_id = history_dir.episodeId local history_fname = history_dir.fname + local history_extra = history_dir.extra local playing_number = nil if history_fname then @@ -1222,10 +1235,16 @@ function auto_load_danmaku(path, dir, filename, number) end if playing_number ~= nil then local x = playing_number - history_number --获取集数差值 - local tmp_id = tostring(x + history_id) danmaku.episode = episode_number and string.format("第%s话", episode_number + x) or history_dir.episodeTitle show_message("自动加载上次匹配的弹幕", 3) - set_episode_id(tmp_id) + if history_id then + local tmp_id = tostring(x + history_id) + set_episode_id(tmp_id) + elseif history_extra and x ~= 0 then + local episodenum = history_extra.episodenum + x + get_details(history_extra.class, history_extra.id, history_extra.site, + history_extra.title, history_extra.year, history_extra.number, episodenum) + end else get_danmaku_with_hash(filename, path) end diff --git a/extra.lua b/extra.lua new file mode 100644 index 0000000..81ec71c --- /dev/null +++ b/extra.lua @@ -0,0 +1,346 @@ +local utils = require 'mp.utils' +local msg = require 'mp.msg' + +local Source = { + ["b 站"] = "bilibili1", + ["腾讯"] = "qq", + ["爱奇艺"] = "qiyi", + ["优酷"] = "youku", +} + +local function is_chinese(str) + return string.match(str, "[\228-\233][\128-\191]") ~= nil +end + +local function load_extra_danmaku(url, episode, number, class, id, site, title, year) + local play_url = nil + if url:match("^.-%.html") then + play_url = url:match("^(.-%.html).*") + else + play_url = url:gsub("%?bsource=360ogvys$","") + end + danmaku.anime = title .. " (" .. year .. ")" + danmaku.episode = "第" .. episode .. "话" + danmaku.source = "extra" + danmaku.extra = {} + danmaku.extra.id = id + danmaku.extra.site = site + danmaku.extra.year = year + danmaku.extra.class = class + danmaku.extra.title = title + danmaku.extra.number = number + danmaku.extra.episodenum = episode + write_history() + add_danmaku_source(play_url, true) +end + +local function query_tmdb_movies(title, menu) + local encoded_title = url_encode(title) + local url = string.format("https://api.themoviedb.org/3/search/movie?api_key=%s&query=%s&language=zh-CN", + options.tmdb_api_key, encoded_title) + + local cmd = { + "curl", + "-s", + "-H", "accept: application/json", + url + } + + local res = mp.command_native({ + name = "subprocess", + args = cmd, + capture_stdout = true, + capture_stderr = true, + }) + + if res.status ~= 0 then + local message = "获取 tmdb 中文数据失败" + if uosc_available then + update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, title) + else + show_message(message, 3) + end + msg.error("获取 tmdb 中文数据失败:" .. res.stderr) + end + + local data = utils.parse_json(res.stdout) + if data and data.results and #data.results > 0 then + return data.results[1].name + end +end + +local function query_tmdb_tv(title, menu) + local encoded_title = url_encode(title) + local url = string.format("https://api.themoviedb.org/3/search/tv?api_key=%s&query=%s&language=zh-CN", + options.tmdb_api_key, encoded_title) + + local cmd = { + "curl", + "-s", + "-H", "accept: application/json", + url + } + + local res = mp.command_native({ + name = "subprocess", + args = cmd, + capture_stdout = true, + capture_stderr = true, + }) + + if res.status ~= 0 then + local message = "获取 tmdb 中文数据失败" + if uosc_available then + update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, title) + else + show_message(message, 3) + end + msg.error("获取 tmdb 中文数据失败:" .. res.stderr) + end + + local data = utils.parse_json(res.stdout) + if data and data.results and #data.results > 0 then + return data.results[1].name + end +end + +local function get_episode_number(cat, id, site) + local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&site=%s", + cat, id, site) + + local cmd = { "curl", "-s", url } + local res = mp.command_native({ + name = "subprocess", + args = cmd, + capture_stdout = true, + capture_stderr = true, + }) + + if res.status ~= 0 then + msg.error("Failed to fetch data: " .. (res.stderr or "unknown error")) + return nil + end + + local result = utils.parse_json(res.stdout) + if result and result.data and result.data.allupinfo then + return tonumber(result.data.allupinfo[site]) + end + return nil +end + +function get_details(class, id, site, title, year, number, episodenum) + local message = episodenum and "查询弹幕中..." or "加载数据中..." + local menu_type = "menu_details" + local menu_title = "剧集信息" + local footnote = "使用 / 打开筛选" + if uosc_available and not episodenum then + update_menu_uosc(menu_type, menu_title, message, footnote) + else + show_message(message, 3) + end + + local cat = 0 + if class == "电影" then + cat = 1 + elseif class == "电视剧" then + cat = 2 +-- elseif class == "综艺" then +-- cat = 3 + elseif class == "动漫" then + cat = 4 + end + + if not number then + number = get_episode_number(cat, id, site) + end + if not number or cat == 0 then + local message = "剧集信息获取失败..." + if uosc_available and not episodenum then + update_menu_uosc(menu_type, menu_title, message, footnote) + else + show_message(message, 3) + end + end + + local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&start=1&end=%s&site=%s", + cat, id, number, site) + + local cmd = { "curl", "-s", url } + local res = mp.command_native({ + name = "subprocess", + args = cmd, + capture_stdout = true, + capture_stderr = true, + }) + + if res.status ~= 0 then + local message = "剧集信息获取失败..." + if uosc_available and not episodenum then + update_menu_uosc(menu_type, menu_title, message, footnote) + else + show_message(message, 3) + end + msg.error("Failed to fetch data: " .. (res.stderr or "unknown error")) + end + + local result = utils.parse_json(res.stdout) + local items = {} + if result and result.data and result.data.allepidetail then + local data = result.data.allepidetail + local playurl, episode = nil, nil + if episodenum then + for _, item in ipairs(data[site]) do + if tonumber(item.playlink_num) == tonumber(episodenum) then + playurl = item.url + episode = item.playlink_num + break + end + end + if playurl then + load_extra_danmaku(playurl, episode, number, class, id, site, title, year) + return + end + end + for _, item in ipairs(data[site]) do + table.insert(items, { + title = "第" .. item.playlink_num .. "集", + hint = item.playlink_num, + value = { + "script-message-to", + mp.get_script_name(), + "add-extra-event", + item.url, item.playlink_num, number, class, id, site, title, year + }, + }) + end + end + if #items > 0 then + if uosc_available then + update_menu_uosc(menu_type, menu_title, items, footnote) + else + show_message("", 0) + mp.add_timeout(0.1, function() + open_menu_select(items) + end) + end + end +end + +local function search_query(query, class, menu) + local url = string.format("https://api.so.360kan.com/index?force_v=1&kw=%s", query) + if class ~= nil then + url = url .. "&type=" .. class + end + local cmd = { "curl", "-s", url } + + local res = mp.command_native({ + name = "subprocess", + args = cmd, + capture_stdout = true, + capture_stderr = true, + }) + + if res.status ~= 0 then + local message = "获取数据失败" + if uosc_available then + update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, query) + else + show_message(message, 3) + end + msg.error("HTTP 请求失败:" .. res.stderr) + end + + local result = utils.parse_json(res.stdout) + local items = {} + if result and result.data.longData and result.data.longData.rows then + for _, item in ipairs(result.data.longData.rows) do + if item.playlinks then + for source_name, source_id in pairs(Source) do + if item.playlinks[source_id] then + table.insert(items, { + title = item.titleTxt, + hint = item.cat_name .. " | " .. item.year .. " | 来源:" .. source_name, + value = { + "script-message-to", + mp.get_script_name(), + "get-extra-event", + item.cat_name, item.en_id, utils.format_json(item.playlinks), source_id, + item.titleTxt, item.year, + }, + }) + end + end + end + end + end + if #items > 0 then + if uosc_available then + update_menu_uosc(menu.type, menu.title, items, menu.footnote, menu.cmd, query) + else + show_message("", 0) + mp.add_timeout(0.1, function() + open_menu_select(items) + end) + end + end + return nil +end + +function query_extra(name, class) + local title = nil + local class = class and class:lower() + local menu = {} + local message = "加载数据中..." + menu.type = "menu_anime" + menu.title = "在此处输入番剧名称" + menu.footnote = "使用enter或ctrl+enter进行搜索" + menu.cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" } + if uosc_available then + update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, name) + else + show_message(message, 30) + end + + if is_chinese(name) then + search_query(name, class, menu) + return + end + + if class == "dy" then + title = query_tmdb_movies(name, menu) + else + title = query_tmdb_tv(name, menu) + end + + if title then + search_query(title, class) + end +end + +mp.register_script_message("get-extra-event", function(cat, id, playlinks, source_id, title, year) + if uosc_available then + mp.commandv("script-message-to", "uosc", "close-menu", "menu_anime") + end + if cat == "电影" then + local playlinks = utils.parse_json(playlinks) + if playlinks[source_id]:match("^.-%.html") then + play_url = playlinks[source_id]:match("^(.-%.html).*") + else + play_url = playlinks[source_id]:gsub("%?bsource=360ogvys$","") + end + danmaku.anime = title .. " (" .. year .. ")" + danmaku.episode = "电影" + danmaku.source = "extra" + write_history() + add_danmaku_source(play_url, true) + else + get_details(cat, id, source_id, title, year) + end +end) + +mp.register_script_message("add-extra-event", function(url, episode, number, class, id, site, title, year) + if uosc_available then + mp.commandv("script-message-to", "uosc", "close-menu", "menu_details") + end + load_extra_danmaku(url, episode, number, class, id, site, title, year) +end) diff --git a/main.lua b/main.lua index a1e5e97..6dd5a92 100644 --- a/main.lua +++ b/main.lua @@ -3,10 +3,11 @@ local utils = require("mp.utils") require("options") require("api") +require('extra') require('render') -local input_loaded, input = pcall(require, "mp.input") -local uosc_available = false +input_loaded, input = pcall(require, "mp.input") +uosc_available = false function get_animes(query) local encoded_query = url_encode(query) @@ -16,8 +17,12 @@ function get_animes(query) local items = {} local message = "加载数据中..." + local menu_type = "menu_anime" + local menu_title = "在此处输入番剧名称" + local footnote = "使用enter或ctrl+enter进行搜索" + local menu_cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" } if uosc_available then - update_menu(menu_item(message), query) + update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query) else show_message(message, 30) end @@ -28,7 +33,7 @@ function get_animes(query) if res.status ~= 0 then local message = "获取数据失败" if uosc_available then - update_menu(menu_item(message), query) + update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query) else show_message(message, 3) end @@ -40,7 +45,7 @@ function get_animes(query) if not response or not response.animes then local message = "无结果" if uosc_available then - update_menu(menu_item(message), query) + update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query) else show_message(message, 3) end @@ -62,7 +67,7 @@ function get_animes(query) end if uosc_available then - update_menu(items, query) + update_menu_uosc(menu_type, menu_title, items, footnote, menu_cmd, query) elseif input_loaded then show_message("", 0) mp.add_timeout(0.1, function() @@ -76,22 +81,19 @@ function get_episodes(animeTitle, episodes) for _, episode in ipairs(episodes) do table.insert(items, { title = episode.episodeTitle, - value = { "script-message-to", mp.get_script_name(), "load-danmaku", animeTitle, episode.episodeTitle, episode.episodeId }, + value = { "script-message-to", mp.get_script_name(), "load-danmaku", + animeTitle, episode.episodeTitle, episode.episodeId }, keep_open = false, selectable = true, }) end - local menu_props = { - type = "menu_episodes", - title = "剧集信息", - search_style = "disabled", - items = items, - } + local menu_type = "menu_episodes" + local menu_title = "剧集信息" + local footnote = "使用 / 打开筛选" - local json_props = utils.format_json(menu_props) if uosc_available then - mp.commandv("script-message-to", "uosc", "open-menu", json_props) + update_menu_uosc(menu_type, menu_title, items, footnote) elseif input_loaded then mp.add_timeout(0.1, function() open_menu_select(items) @@ -99,20 +101,28 @@ function get_episodes(animeTitle, episodes) end end -function menu_item(input) +function update_menu_uosc(menu_type, title, item, footnote, cmd, query) local items = {} - table.insert(items, { title = input, value = "" }) - return items -end + if type(item) == "string" then + table.insert(items, { + title = item, + value = "", + italic = true, + keep_open = true, + selectable = false, + align = "center", + }) + else + items = item + end -function update_menu(items, query) local menu_props = { - type = "menu_anime", - title = "在此处输入番剧名称", - search_style = "palette", + type = menu_type, + title = title, + search_style = cmd and "palette" or "on_demand", search_debounce = "submit", - on_search = { "script-message-to", mp.get_script_name(), "search-anime-event" }, - footnote = "使用enter或ctrl+enter进行搜索", + on_search = cmd, + footnote = footnote, search_suggestion = query, items = items, } @@ -162,6 +172,12 @@ function open_input_menu_uosc() } end + items[#items + 1] = { + hint = "追加|ds或|dy或|dm可搜索电视剧|电影|国漫", + keep_open = true, + selectable = false, + } + local menu_props = { type = "menu_danmaku", title = "在此处输入番剧名称", @@ -242,8 +258,10 @@ local menu_items_config = { fontsize = { title = "大小", query = "fontsize", hint = options.fontsize, scope = { min = "0", max = "inf"} }, outline = { title = "描边", query = "outline", hint = options.outline, scope = { min = "0.0", max = "4.0"} }, shadow = { title = "阴影", query = "shadow", hint = options.shadow, scope = { min = "0", max = "inf"} }, - transparency = { title = "透明度", query = "transparency", hint = options.transparency, scope = { min = "0", max = "255"} }, - displayarea = { title = "弹幕显示范围", query = "displayarea", hint = options.displayarea, scope = { min = "0.0", max = "1.0"} }, + transparency = { title = "透明度", query = "transparency", + hint = options.transparency, scope = { min = "0", max = "255"} }, + displayarea = { title = "弹幕显示范围", query = "displayarea", + hint = options.displayarea, scope = { min = "0.0", max = "1.0"} }, } -- 创建一个包含键顺序的表,这是样式菜单的排布顺序 local ordered_keys = {"bold", "fontsize", "outline", "shadow", "transparency", "displayarea"} @@ -504,7 +522,12 @@ mp.register_script_message("search-anime-event", function(query) if uosc_available then mp.commandv("script-message-to", "uosc", "close-menu", "menu_danmaku") end - get_animes(query) + local name, class = query:match("^(.-)%s*|%s*(.-)%s*$") + if name and class then + query_extra(name, class) + else + get_animes(query) + end end) mp.register_script_message("search-episodes-event", function(animeTitle, episodes) if uosc_available then @@ -516,7 +539,7 @@ end) -- Register script message to show the input menu mp.register_script_message("load-danmaku", function(animeTitle, episodeTitle, episodeId) danmaku.anime = animeTitle - danmaku.episode = episodeTitle:match("(第%d+[话回集]+)") or episodeTitle + danmaku.episode = episodeTitle set_episode_id(episodeId, true) end) diff --git a/options.lua b/options.lua index 116f6e4..44b5a68 100644 --- a/options.lua +++ b/options.lua @@ -3,6 +3,9 @@ local opt = require("mp.options") -- 选项 options = { api_server = "https://api.dandanplay.net", + -- 设置 tmdb 的 API Key,用于获取非动画条目的中文信息(当搜索内容非中文时) + -- 请在 https://api.dandanplay.net 注册后获取 + tmdb_api_key= "", load_more_danmaku = false, auto_load = false, autoload_local_danmaku = false, diff --git a/render.lua b/render.lua index 07644a1..1be80f3 100644 --- a/render.lua +++ b/render.lua @@ -281,7 +281,8 @@ function show_message(text, time) message_timer.timeout = time or 3 message_timer:kill() message_overlay:remove() - local message = string.format("{\\an%d\\pos(%d,%d)}%s", options.message_anlignment, options.message_x, options.message_y, text) + local message = string.format("{\\an%d\\pos(%d,%d)}%s", options.message_anlignment, + options.message_x, options.message_y, text) local width, height = 1920, 1080 local ratio = osd_width / osd_height if width / height < ratio then