您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download subtitles (transcript with timestamp) from LingQ reader pages
- // ==UserScript==
- // @name LingQ Subtitle Downloader
- // @namespace http://tampermonkey.net/
- // @version 2.0
- // @description Download subtitles (transcript with timestamp) from LingQ reader pages
- // @author Yuxin with ChatGPT
- // @match https://www.lingq.com/*
- // @match https://*lingq.com/*
- // @run-at document-start
- // @grant none
- // @license MIT
- // ==/UserScript==
- (function() {
- 'use strict';
- console.log("LingQ Subtitle Downloader Active Request script started.");
- // Extract API URL from the current page URL.
- // For example, from "https://www.lingq.com/en/learn/es/web/listen/1003504" we derive:
- // - content language: es
- // - lesson ID: 1003504
- // and build: "https://www.lingq.com/api/v3/es/lessons/1003504/simple/?"
- function getApiUrl() {
- const m = window.location.href.match(/^https:\/\/www\.lingq\.com\/([^\/]+)\/learn\/([^\/]+)\/web\/(listen|reader)\/(\d+)/);
- if (m) {
- const contentLang = m[2]; // second segment after "learn"
- const lessonId = m[4];
- const apiUrl = `https://www.lingq.com/api/v3/${contentLang}/lessons/${lessonId}/simple/?`;
- console.log("Constructed API URL:", apiUrl);
- return apiUrl;
- }
- console.warn("Could not parse lesson API URL from:", window.location.href);
- return null;
- }
- // Convert seconds to SRT timestamp format (HH:MM:SS,mmm)
- function formatTime(seconds) {
- let hours = Math.floor(seconds / 3600);
- let minutes = Math.floor((seconds % 3600) / 60);
- let secs = Math.floor(seconds % 60);
- let ms = Math.floor((seconds % 1) * 1000);
- return (hours < 10 ? "0" + hours : hours) + ":" +
- (minutes < 10 ? "0" + minutes : minutes) + ":" +
- (secs < 10 ? "0" + secs : secs) + "," +
- (ms < 100 ? (ms < 10 ? "00" + ms : "0" + ms) : ms);
- }
- // Build SRT content from the fetched lesson data
- function buildSRT(data) {
- let srtContent = "";
- let index = 1;
- data.tokenizedText.forEach(segment => {
- if (segment.length > 0) {
- let item = segment[0];
- let start = item.timestamp[0];
- let end = item.timestamp[1];
- srtContent += index + "\n";
- srtContent += formatTime(start) + " --> " + formatTime(end) + "\n";
- srtContent += item.text + "\n\n";
- index++;
- }
- });
- return srtContent;
- }
- // Trigger download of the SRT file
- function downloadSRT(srtContent, fileName) {
- const blob = new Blob([srtContent], { type: 'text/plain' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = fileName;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }
- // Create and insert the Download Button with improved styling.
- function addDownloadButton() {
- if (document.getElementById('lingqDownloadButton')) return; // Prevent duplicates
- const button = document.createElement('button');
- button.id = 'lingqDownloadButton';
- button.innerText = "Download Subtitle";
- // Style: bottom right with background, outline, and custom text color.
- button.style.position = 'fixed';
- button.style.bottom = '10px';
- button.style.right = '10px';
- button.style.zIndex = 9999;
- button.style.padding = '10px 15px';
- button.style.fontSize = '14px';
- button.style.background = '#007bff';
- button.style.color = '#ffffff';
- button.style.border = '2px solid #0056b3';
- button.style.borderRadius = '4px';
- button.style.cursor = 'pointer';
- button.style.boxShadow = '2px 2px 6px rgba(0,0,0,0.2)';
- button.addEventListener('click', function() {
- const apiUrl = getApiUrl();
- if (!apiUrl) {
- alert("Unable to determine lesson API URL.");
- return;
- }
- console.log("Sending request to API URL:", apiUrl);
- fetch(apiUrl, {
- headers: {
- 'accept': 'application/json',
- 'x-lingq-app': 'Web/6.0.10'
- }
- })
- .then(response => response.json())
- .then(data => {
- console.log("Received lesson data:", data);
- const srtContent = buildSRT(data);
- console.log("Generated SRT content:\n", srtContent);
- const fileName = data.title.replace(/[\\\/:*?"<>|]/g, '_') + ".srt";
- downloadSRT(srtContent, fileName);
- })
- .catch(error => {
- console.error("Error fetching lesson data:", error);
- alert("Error fetching lesson data.");
- });
- });
- document.body.appendChild(button);
- }
- // Remove the download button (used when leaving allowed pages)
- function removeDownloadButton() {
- const btn = document.getElementById('lingqDownloadButton');
- if (btn) {
- btn.remove();
- }
- }
- // Update button visibility based on allowed pages (listen/reader pages)
- function updateButtonVisibility() {
- const allowedPattern = /^https:\/\/www\.lingq\.com\/[^\/]+\/learn\/[^\/]+\/web\/(listen|reader)\//;
- if (allowedPattern.test(window.location.href)) {
- addDownloadButton();
- } else {
- removeDownloadButton();
- }
- }
- // Monitor URL changes using history API overrides and popstate events.
- (function() {
- const _wr = function(type) {
- let orig = history[type];
- return function() {
- let rv = orig.apply(this, arguments);
- window.dispatchEvent(new Event('locationchange'));
- return rv;
- };
- };
- history.pushState = _wr('pushState');
- history.replaceState = _wr('replaceState');
- window.addEventListener('popstate', () => {
- window.dispatchEvent(new Event('locationchange'));
- });
- })();
- window.addEventListener('locationchange', () => {
- console.log("Location changed to:", window.location.href);
- updateButtonVisibility();
- });
- window.addEventListener('DOMContentLoaded', () => {
- updateButtonVisibility();
- });
- })();