Local Movie Search

在网页上添加输入框和按钮,搜索本地电影是否存在(需要配合Everything的HTTP服务器使用)。注:拖选要搜索的电影名再使用ALT+C快捷键可直接搜索。

  1. // ==UserScript==
  2. // @name Local Movie Search
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.9
  5. // @description 在网页上添加输入框和按钮,搜索本地电影是否存在(需要配合Everything的HTTP服务器使用)。注:拖选要搜索的电影名再使用ALT+C快捷键可直接搜索。
  6. // @author huangmmd
  7. // @match *://*/*
  8. // @license MIT
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // 创建输入框和按钮
  19. const input = document.createElement('input');
  20. input.placeholder = '输入电影名字';
  21. input.style.padding = '7.744px 11.616px'; // 7.04px * 1.1 = 7.744px, 10.56px * 1.1 = 11.616px
  22. input.style.border = '2.2px solid #ccc'; // 2px * 1.1 = 2.2px
  23. input.style.borderRadius = '3.872px'; // 3.52px * 1.1 = 3.872px
  24. input.style.marginRight = '4.84px'; // 4.4px * 1.1 = 4.84px
  25. input.style.fontSize = '13.552px'; // 12.32px * 1.1 = 13.552px
  26.  
  27. const button = document.createElement('button');
  28. button.textContent = '搜索'; // 修改按钮文本为“搜索”
  29. button.style.backgroundColor = '#007BFF';
  30. button.style.color = 'white';
  31. button.style.border = 'none';
  32. button.style.borderRadius = '4px'; // 调整圆角大小
  33. button.style.cursor = 'pointer';
  34. button.style.fontSize = '12px'; // 调整字体大小为12px
  35. button.style.padding = '6px 12px'; // 调整内边距
  36. button.addEventListener('mouseover', function() {
  37. this.style.backgroundColor = '#0056b3';
  38. });
  39. button.addEventListener('mouseout', function() {
  40. this.style.backgroundColor = '#007BFF';
  41. });
  42.  
  43. // 创建用于显示结果的元素
  44. const resultDiv = document.createElement('div');
  45. resultDiv.style.color = '#333';
  46. resultDiv.style.marginBottom = '5px';
  47. resultDiv.style.fontSize = '13.552px'; // 修改字体大小为与输入框一致
  48. resultDiv.style.maxHeight = '200px'; // 设置最大高度
  49. resultDiv.style.overflowY = 'auto'; // 添加垂直滚动条
  50.  
  51. // 创建用于触发搜索界面显示和隐藏的小方框按钮
  52. const toggleButton = document.createElement('button');
  53. toggleButton.textContent = '搜';
  54. toggleButton.style.padding = '7.744px 11.616px'; // 7.04px * 1.1 = 7.744px, 10.56px * 1.1 = 11.616px
  55. toggleButton.style.backgroundColor = '#007BFF'; // 与搜索按钮颜色一致
  56. toggleButton.style.color = 'white';
  57. toggleButton.style.border = 'none';
  58. toggleButton.style.borderRadius = '3.872px'; // 3.52px * 1.1 = 3.872px
  59. toggleButton.style.cursor = 'pointer';
  60. toggleButton.style.fontSize = '13.552px'; // 12.32px * 1.1 = 13.552px
  61. toggleButton.style.position = 'fixed';
  62. toggleButton.style.bottom = '19.36px'; // 17.6px * 1.1 = 19.36px
  63. toggleButton.style.left = '19.36px'; // 移动到左下角
  64. toggleButton.style.zIndex = 9999;
  65. toggleButton.addEventListener('click', function() {
  66. container.style.display = container.style.display === 'none' ? 'block' : 'none';
  67. toggleButton.style.display = container.style.display === 'none' ? 'block' : 'none'; // 隐藏或显示“搜”字按钮
  68. });
  69. document.body.appendChild(toggleButton);
  70.  
  71. // 将输入框、按钮和结果显示元素添加到页面左下角,并默认隐藏
  72. const container = document.createElement('div');
  73. container.style.position = 'fixed';
  74. container.style.bottom = '19.36px'; // 17.6px * 1.1 = 19.36px
  75. container.style.left = '19.36px'; // 17.6px * 1.1 = 19.36px
  76. container.style.zIndex = 9999;
  77. container.style.backgroundColor = 'white';
  78. container.style.padding = '14.52px'; // 13.2px * 1.1 = 14.52px
  79. container.style.borderRadius = '7.744px'; // 7.04px * 1.1 = 7.744px
  80. container.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
  81. container.style.display = 'none'; // 默认隐藏搜索界面
  82. container.appendChild(resultDiv);
  83. container.appendChild(input);
  84. container.appendChild(button);
  85. document.body.appendChild(container);
  86.  
  87. // 创建设置面板
  88. const settingsContainer = document.createElement('div');
  89. settingsContainer.style.position = 'fixed';
  90. settingsContainer.style.top = '50%';
  91. settingsContainer.style.left = '50%';
  92. settingsContainer.style.transform = 'translate(-50%, -50%)'; // 居中显示
  93. settingsContainer.style.zIndex = 9999;
  94. settingsContainer.style.backgroundColor = 'white';
  95. settingsContainer.style.padding = '15px';
  96. settingsContainer.style.borderRadius = '8px';
  97. settingsContainer.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
  98. settingsContainer.style.display = 'none'; // 默认隐藏设置面板
  99.  
  100. // 添加关闭按钮
  101. const closeButton = document.createElement('button');
  102. closeButton.textContent = '关闭';
  103. closeButton.style.position = 'absolute';
  104. closeButton.style.top = '9.68px'; // 8.8px * 1.1 = 9.68px
  105. closeButton.style.right = '9.68px'; // 8.8px * 1.1 = 9.68px
  106. closeButton.style.padding = '4.84px 9.68px'; // 4.4px * 1.1 = 4.84px, 8.8px * 1.1 = 9.68px
  107. closeButton.style.backgroundColor = '#dc3545';
  108. closeButton.style.color = 'white';
  109. closeButton.style.border = 'none';
  110. closeButton.style.borderRadius = '3.872px'; // 3.52px * 1.1 = 3.872px
  111. closeButton.style.cursor = 'pointer';
  112. closeButton.style.fontSize = '11.616px'; // 10.56px * 1.1 = 11.616px
  113. closeButton.addEventListener('click', function() {
  114. settingsContainer.style.display = 'none';
  115. });
  116. settingsContainer.appendChild(closeButton);
  117.  
  118. const settingsLabel = document.createElement('label');
  119. settingsLabel.textContent = '检索服务器地址: ';
  120. settingsLabel.style.display = 'block';
  121. settingsLabel.style.marginBottom = '4.84px'; // 4.4px * 1.1 = 4.84px
  122.  
  123. const settingsInput = document.createElement('input');
  124. settingsInput.type = 'text';
  125. settingsInput.value = 'http://localhost:8080'; // 默认值
  126. settingsInput.style.width = '193.6px'; // 176px * 1.1 = 193.6px
  127. settingsInput.style.padding = '7.744px 11.616px'; // 7.04px * 1.1 = 7.744px, 10.56px * 1.1 = 11.616px
  128. settingsInput.style.border = '2px solid #ccc';
  129. settingsInput.style.borderRadius = '3.872px'; // 3.52px * 1.1 = 3.872px
  130. settingsInput.style.marginRight = '4.84px'; // 4.4px * 1.1 = 4.84px
  131. settingsInput.style.fontSize = '13.552px'; // 12.32px * 1.1 = 13.552px
  132.  
  133. const settingsSaveButton = document.createElement('button');
  134. settingsSaveButton.textContent = '保存';
  135. settingsSaveButton.style.padding = '7.744px 11.616px'; // 7.04px * 1.1 = 7.744px, 10.56px * 1.1 = 11.616px
  136. settingsSaveButton.style.backgroundColor = '#007BFF';
  137. settingsSaveButton.style.color = 'white';
  138. settingsSaveButton.style.border = 'none';
  139. settingsSaveButton.style.borderRadius = '3.872px'; // 3.52px * 1.1 = 3.872px
  140. settingsSaveButton.style.cursor = 'pointer';
  141. settingsSaveButton.style.fontSize = '13.552px'; // 12.32px * 1.1 = 13.552px
  142.  
  143. settingsContainer.appendChild(settingsLabel);
  144. settingsContainer.appendChild(settingsInput);
  145. settingsContainer.appendChild(settingsSaveButton);
  146. document.body.appendChild(settingsContainer);
  147.  
  148. // 加载保存的服务器地址
  149. const savedUrl = localStorage.getItem('searchServerUrl');
  150. if (savedUrl) {
  151. settingsInput.value = savedUrl; // 恢复用户设置的地址
  152. }
  153.  
  154. // 使用 GM_registerMenuCommand 创建设置菜单
  155. GM_registerMenuCommand('设置检索服务器地址', function() {
  156. settingsContainer.style.display = settingsContainer.style.display === 'none' ? 'block' : 'none';
  157. });
  158.  
  159. // 修改保存按钮逻辑,确保地址持久化并全局生效
  160. settingsSaveButton.addEventListener('click', function() {
  161. const newUrl = settingsInput.value.trim();
  162. if (newUrl) {
  163. localStorage.setItem('searchServerUrl', newUrl); // 持久化到 localStorage
  164. settingsContainer.style.display = 'none';
  165. } else {
  166. alert('请输入有效的检索服务器地址');
  167. }
  168. });
  169.  
  170. // 在页面加载时同步检索服务器地址到输入框
  171. window.addEventListener('load', function() {
  172. const globalSavedUrl = localStorage.getItem('searchServerUrl');
  173. if (globalSavedUrl) {
  174. settingsInput.value = globalSavedUrl; // 确保所有页面加载时使用相同的地址
  175. }
  176. });
  177.  
  178. // 创建新的设置面板
  179. const websiteSettingsContainer = document.createElement('div');
  180. websiteSettingsContainer.style.position = 'fixed';
  181. websiteSettingsContainer.style.top = '50%';
  182. websiteSettingsContainer.style.left = '50%';
  183. websiteSettingsContainer.style.transform = 'translate(-50%, -50%)'; // 居中显示
  184. websiteSettingsContainer.style.zIndex = 9999;
  185. websiteSettingsContainer.style.backgroundColor = 'white';
  186. websiteSettingsContainer.style.padding = '15px';
  187. websiteSettingsContainer.style.borderRadius = '8px';
  188. websiteSettingsContainer.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
  189. websiteSettingsContainer.style.display = 'none'; // 默认隐藏设置面板
  190.  
  191. // 添加关闭按钮
  192. const websiteCloseButton = document.createElement('button');
  193. websiteCloseButton.textContent = '关闭';
  194. websiteCloseButton.style.position = 'absolute';
  195. websiteCloseButton.style.top = '9.68px'; // 8.8px * 1.1 = 9.68px
  196. websiteCloseButton.style.right = '9.68px'; // 8.8px * 1.1 = 9.68px
  197. websiteCloseButton.style.padding = '4.84px 9.68px'; // 4.4px * 1.1 = 4.84px, 8.8px * 1.1 = 9.616px
  198. websiteCloseButton.style.backgroundColor = '#dc3545';
  199. websiteCloseButton.style.color = 'white';
  200. websiteCloseButton.style.border = 'none';
  201. websiteCloseButton.style.borderRadius = '3.872px'; // 3.52px * 1.1 = 3.872px
  202. websiteCloseButton.style.cursor = 'pointer';
  203. websiteCloseButton.style.fontSize = '11.616px'; // 10.56px * 1.1 = 11.616px
  204. websiteCloseButton.addEventListener('click', function() {
  205. websiteSettingsContainer.style.display = 'none';
  206. });
  207. websiteSettingsContainer.appendChild(websiteCloseButton);
  208.  
  209. const websiteSettingsLabel = document.createElement('label');
  210. websiteSettingsLabel.textContent = '自动搜索网站列表 (每行一个URL): ';
  211. websiteSettingsLabel.style.display = 'block';
  212. websiteSettingsLabel.style.marginBottom = '4.84px'; // 4.4px * 1.1 = 4.84px
  213.  
  214. const websiteSettingsTextarea = document.createElement('textarea');
  215. websiteSettingsTextarea.style.width = '300px'; // 设置宽度
  216. websiteSettingsTextarea.style.height = '100px'; // 设置高度
  217. websiteSettingsTextarea.style.padding = '7.744px 11.616px'; // 7.04px * 1.1 = 7.744px, 10.56px * 1.1 = 11.616px
  218. websiteSettingsTextarea.style.border = '2px solid #ccc';
  219. websiteSettingsTextarea.style.borderRadius = '3.872px'; // 3.52px * 1.1 = 3.872px
  220. websiteSettingsTextarea.style.marginRight = '4.84px'; // 4.4px * 1.1 = 4.84px
  221. websiteSettingsTextarea.style.fontSize = '13.552px'; // 12.32px * 1.1 = 13.552px
  222.  
  223. // 加载保存的网站列表
  224. const savedWebsites = GM_getValue('autoSearchWebsites', '');
  225. websiteSettingsTextarea.value = savedWebsites;
  226.  
  227. const websiteSettingsSaveButton = document.createElement('button');
  228. websiteSettingsSaveButton.textContent = '保存';
  229. websiteSettingsSaveButton.style.padding = '7.744px 11.616px'; // 7.04px * 1.1 = 7.744px, 10.56px * 1.1 = 11.616px
  230. websiteSettingsSaveButton.style.backgroundColor = '#007BFF';
  231. websiteSettingsSaveButton.style.color = 'white';
  232. websiteSettingsSaveButton.style.border = 'none';
  233. websiteSettingsSaveButton.style.borderRadius = '3.872px'; // 3.52px * 1.1 = 3.872px
  234. websiteSettingsSaveButton.style.cursor = 'pointer';
  235. websiteSettingsSaveButton.style.fontSize = '13.552px'; // 12.32px * 1.1 = 13.552px
  236. websiteSettingsSaveButton.addEventListener('click', function() {
  237. GM_setValue('autoSearchWebsites', websiteSettingsTextarea.value);
  238. websiteSettingsContainer.style.display = 'none';
  239. });
  240.  
  241. websiteSettingsContainer.appendChild(websiteSettingsLabel);
  242. websiteSettingsContainer.appendChild(websiteSettingsTextarea);
  243. websiteSettingsContainer.appendChild(websiteSettingsSaveButton);
  244. document.body.appendChild(websiteSettingsContainer);
  245.  
  246. // 使用 GM_registerMenuCommand 创建设置菜单
  247. GM_registerMenuCommand('设置自动搜索网站列表', function() {
  248. websiteSettingsContainer.style.display = websiteSettingsContainer.style.display === 'none' ? 'block' : 'none';
  249. });
  250.  
  251. // 按钮点击事件处理函数
  252. function performSearch() {
  253. const movieName = input.value.trim();
  254. if (movieName) {
  255. // 检查电影名字是否包含“[]”或“《》”或“<>”符号
  256. const shouldSplit = !/[\[\]《》<>]/.test(movieName);
  257.  
  258. // 根据检查结果决定是否将电影名字按空格拆分成多个部分
  259. const movieParts = shouldSplit ? movieName.split(' ') : [movieName];
  260.  
  261. // 存储所有找到的电影和未找到的电影
  262. let allFoundMovies = [];
  263. let notFoundMovies = [];
  264.  
  265. // 遍历每个部分进行搜索
  266. movieParts.forEach(part => {
  267. if (part) {
  268. const searchUrl = `${settingsInput.value}/?s=${encodeURIComponent(part)}`;
  269.  
  270. GM_xmlhttpRequest({
  271. method: 'GET',
  272. url: searchUrl,
  273. onload: function(response) {
  274. // 修改 commonFormats 数组,增加对 .zip 和 .epub 格式的支持
  275. const commonFormats = ['.mp4', '.mkv', '.avi', '.mov', '.flv', '.wmv', '.zip', '.epub'];
  276. let foundMovies = [];
  277. commonFormats.forEach(format => {
  278. const regex = new RegExp(`[^\\s"']+${format}`, 'g');
  279. let matches = response.responseText.match(regex);
  280. if (matches) {
  281. foundMovies = foundMovies.concat(matches);
  282. }
  283. });
  284.  
  285. // 过滤掉包含 /favi 的结果
  286. foundMovies = foundMovies.filter(movie => !movie.includes('/favi'));
  287.  
  288. // 将找到的电影添加到所有找到的电影数组中
  289. if (foundMovies.length > 0) {
  290. allFoundMovies = allFoundMovies.concat(foundMovies);
  291. } else {
  292. notFoundMovies.push(part);
  293. }
  294.  
  295. // 显示所有找到的电影和未找到的电影
  296. let resultText = '';
  297. if (allFoundMovies.length > 0) {
  298. resultText += '本地存在以下电影、漫画:<br>';
  299. allFoundMovies.forEach(movie => {
  300. let decodedMovie = decodeURIComponent(movie);
  301. // 提取文件路径和文件名
  302. let filePath = decodedMovie.substring(0, decodedMovie.lastIndexOf('/'));
  303. let fileName = decodedMovie.substring(decodedMovie.lastIndexOf('/') + 1);
  304. // 设置 title 属性为文件名
  305. resultText += `<span title="${fileName}">${filePath}</span><br>`;
  306. });
  307. }
  308. if (notFoundMovies.length > 0) {
  309. if (allFoundMovies.length > 0) {
  310. resultText += '<br>';
  311. }
  312. resultText += '<span style="color: red;">本地不存在以下电影、漫画:</span><br>'; // 修改:标红显示
  313. notFoundMovies.forEach(movie => {
  314. resultText += `<span style="color: red;">${movie}</span><br>`; // 修改:标红显示
  315. });
  316. }
  317. resultDiv.innerHTML = resultText;
  318.  
  319. // 添加点击事件监听器,点击结果显示页的其他位置时清除结果显示
  320. document.addEventListener('click', function clearResult(event) {
  321. if (!resultDiv.contains(event.target)) {
  322. resultDiv.textContent = '';
  323. document.removeEventListener('click', clearResult);
  324. }
  325. });
  326.  
  327. // 根据结果显示情况设置自动消失
  328. },
  329. onerror: function() {
  330. resultDiv.textContent = '请求失败,请检查 Everything 服务器是否正常运行';
  331. }
  332. });
  333. }
  334. });
  335. } else {
  336. resultDiv.textContent = '请输入电影名字';
  337. }
  338. }
  339.  
  340. button.addEventListener('click', performSearch);
  341.  
  342. // 绑定快捷键 ALT + C
  343. document.addEventListener('keydown', function(event) {
  344. if (event.altKey && event.key === 'c') {
  345. const selectedText = window.getSelection().toString().trim();
  346. if (selectedText) {
  347. input.value = selectedText;
  348. // 显示搜索界面
  349. container.style.display = 'block';
  350. toggleButton.style.display = 'none'; // 隐藏“搜”字按钮
  351. // 模拟按钮点击事件进行搜索
  352. performSearch();
  353. }
  354. }
  355. });
  356.  
  357. // 监听输入框内容变化
  358. input.addEventListener('input', function() {
  359. if (input.value.trim() === '') {
  360. container.style.display = 'none'; // 隐藏搜索页面
  361. toggleButton.style.display = 'block'; // 显示“搜”字按钮
  362. }
  363. });
  364.  
  365. // 检查是否是豆瓣电影页面并提取电影名称
  366. function extractMovieNameFromDouban() {
  367. const movieNameElement = document.querySelector('span[property="v:itemreviewed"]');
  368. if (movieNameElement) {
  369. return movieNameElement.textContent.trim();
  370. }
  371. return null;
  372. }
  373.  
  374. // 检查是否是漫画页面并提取漫画名称
  375. function extractMangaNameFromPage() {
  376. const mangaNameElement = document.querySelector('font.text_bglight_big');
  377. if (mangaNameElement) {
  378. return mangaNameElement.textContent.trim();
  379. }
  380. return null;
  381. }
  382.  
  383. // 新增:检查页面是否存在 <h2>*****</h2> 元素并提取文件名
  384. function extractFileNameFromH2() {
  385. const h2Element = document.querySelector('h2');
  386. if (h2Element) {
  387. return h2Element.textContent.trim();
  388. }
  389. return null;
  390. }
  391.  
  392. // 新增:检查页面是否存在 <span style="color:#CC0000;">*******</span> 元素并提取文件名
  393. function extractFileNameFromSpan() {
  394. const spanElement = document.querySelector('span[style="color:#CC0000;"]');
  395. if (spanElement) {
  396. return spanElement.textContent.trim();
  397. }
  398. return null;
  399. }
  400.  
  401. // 自动填充并搜索电影或漫画名称
  402. function autoSearchDoubanMovie() {
  403. const currentUrl = window.location.href;
  404. const savedWebsites = GM_getValue('autoSearchWebsites', '');
  405. const websites = savedWebsites ? savedWebsites.split('\n').map(url => url.trim()) : [];
  406.  
  407. // 新增:增加额外检查,确保 websites 列表有效且非空
  408. if (!websites || websites.length === 0 || websites.every(url => !url)) {
  409. console.log('自动搜索网站列表为空或无效,跳过自动搜索');
  410. return;
  411. }
  412.  
  413. // 检查当前页面是否属于自动搜索网站列表
  414. if (websites.some(website => currentUrl.includes(website))) {
  415. if (currentUrl.includes('https://movie.douban.com/')) {
  416. const name = extractMovieNameFromDouban();
  417. if (name) {
  418. input.value = name;
  419. // 显示搜索界面
  420. container.style.display = 'block';
  421. toggleButton.style.display = 'none'; // 隐藏“搜”字按钮
  422. performSearch();
  423. }
  424. } else {
  425. let name = extractFileNameFromSpan(); // 优先尝试提取 <span style="color:#CC0000;">*******</span> 元素中的文件名
  426. if (!name) {
  427. name = extractMovieNameFromDouban();
  428. }
  429. if (!name) {
  430. name = extractMangaNameFromPage();
  431. }
  432. if (!name) {
  433. name = extractFileNameFromH2(); // 最后尝试提取 <h2> 元素中的文件名
  434. }
  435. if (name) {
  436. input.value = name;
  437. // 显示搜索界面
  438. container.style.display = 'block';
  439. toggleButton.style.display = 'none'; // 隐藏“搜”字按钮
  440. performSearch();
  441. }
  442. }
  443. }
  444. }
  445.  
  446. // 在页面加载完成后尝试自动搜索豆瓣电影或漫画
  447. window.addEventListener('load', function() {
  448. autoSearchDoubanMovie();
  449. });
  450.  
  451. // 添加全局点击事件监听器
  452. document.addEventListener('click', function(event) {
  453. if (input.value.trim() === '' && !container.contains(event.target) && !toggleButton.contains(event.target)) {
  454. container.style.display = 'none'; // 隐藏插件页面
  455. toggleButton.style.display = 'block'; // 显示切换按钮
  456. }
  457. });
  458.  
  459. })();