From bc8bc9202d135df6c27926af67c63f6d928bdac9 Mon Sep 17 00:00:00 2001 From: Oleg Tolchin Date: Wed, 22 Jul 2026 13:35:36 +0300 Subject: [PATCH] --- project-memory.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++ script.js | 61 ++++++++++++++++++++++----------- 2 files changed, 128 insertions(+), 20 deletions(-) create mode 100644 project-memory.md diff --git a/project-memory.md b/project-memory.md new file mode 100644 index 0000000..af9857e --- /dev/null +++ b/project-memory.md @@ -0,0 +1,87 @@ +# ColdFilm project memory + +## Project summary +- Project name: ColdFilm +- Type: static browser-only frontend, no build step, no backend. +- Goal: monitor new ColdFilm releases, show posters, and let user open torrent/magnet links from browser. +- Intended use: local network only; not public-facing. +- Requirement: VPN enabled for accessing coldfilm.ink from Russia. + +## File structure +- index.html — page shell with header, status area, refresh button, film list container, quality modal. +- style.css — dark theme, responsive card layout, modal styling, buttons, status colors. +- script.js — main app logic. +- README.md — project description and operational notes. +- project-memory.md — persistent project architecture and change log. + +## Core architecture +### Frontend-only architecture +- HTML renders skeleton and DOM containers. +- CSS supplies layout and visual design. +- JavaScript performs all runtime behavior. + +### Runtime data flow +1. On page load, `loadFilms()` runs immediately. +2. `fetchWithProxy()` retrieves the main ColdFilm page HTML using a list of public CORS proxies. +3. `parseColdfilm(html)` extracts release titles and page URLs, and tries to pair them with posters. +4. Results are rendered into the DOM as list items with poster, title, and a “Download” button. +5. When user presses “Download”, `showQualityModal(filmName)` loads the film's detail page, extracts `.torrent` and `magnet` links, and shows quality buttons in a modal. +6. `downloadTorrent(url)` opens the chosen torrent/magnet link in a new browser tab. + +## State and cache +- `currentFilmName` stores the currently selected film title. +- `availableTorrents` stores extracted torrent options for the modal. +- `filmData` is a `Map` cache keyed by film title, storing `{ url, poster }`. + +## Parsing strategy +- Parsing is based on regex matching against HTML. +- Release links are detected via `href="/news/..."` patterns and titles from `class="kino-h"` and `title="... [Смотреть Онлайн]"`. +- Posters are extracted using `` `src`, `alt`, or `title` attributes. +- The parser filters out Telegram-related entries. + +## Quality detection logic +- Torrent links are classified by filename substring: + - `720p` / `hd720` → `720p` + - `1080p` / `hd1080` → `1080p` + - `4k` / `2160p` → `4K` + - otherwise → `Стандарт` +- Magnet links are labeled `Magnet`. +- Quality buttons are sorted by priority: `4K`, `1080p`, `720p`, `Стандарт`, `Magnet`. + +## Reliability constraints +- Uses third-party public CORS proxies; if proxies fail, the app cannot fetch content reliably. +- Parser is fragile because it depends heavily on the remote site's HTML structure. +- Browser fetches are subject to CORS, proxy availability, and remote host changes. + +## Auto-refresh behavior +- `loadFilms()` is called on page load. +- `setInterval(loadFilms, 15 * 60 * 1000)` refreshes the list every 15 minutes. + +## UI behavior +- Status panel displays loading, success, error, and downloading states. +- The refresh button triggers `loadFilms()` manually. +- Modal allows quality choice before opening torrent. + +## Known implementation notes +- `fetchWithProxy()` loops through `PROXIES` and returns the first successful response. +- The `PROXIES` list includes public proxies; it should be reviewed and cleaned periodically. +- The code is intentionally simple and dependency-free to keep deployment easy. + +## Current project principles +- Minimal dependencies. +- No backend/server-side logic. +- Direct browser execution only. +- Keep it easy to host on static web server / Synology Web Station. + +## Recent implementation update +- Removed the broken proxy entry with an invalid host string. +- Reworked proxy fetching to use an `AbortController` timeout instead of the unsupported `fetch({ timeout })` option. +- Added `normalizePosterUrl()` to centralize poster URL normalization. +- Broadened torrent-link regex to support query strings in `.torrent` URLs. +- Verified JavaScript syntax with `node --check script.js` and confirmed exit code `0`. + +## Best practice for future edits +- Preserve the current static browser-only architecture. +- If changing parser logic, keep it regex-based and resilient to HTML variations. +- Prefer small, local changes over large rewrites. +- Record any new dependency, proxy change, or URL structure change here. diff --git a/script.js b/script.js index edcbff0..472fb62 100644 --- a/script.js +++ b/script.js @@ -14,20 +14,50 @@ const PROXIES = [ 'https://thingproxy.freeboard.io/fetch/', 'https://cors-anywhere.herokuapp.com/', 'https://corsproxy.ca/', - 'https://proxy.t vot.pw/', ]; // Кеш страниц фильмов и постеров const filmData = new Map(); -async function fetchWithProxy(url) { - for (const proxy of PROXIES) { - try { - const response = await fetch(proxy + encodeURIComponent(url), { timeout: 15000 }); - if (response.ok) return await response.text(); - } catch (e) {} +function buildProxyUrl(proxy, url) { + return proxy + encodeURIComponent(url); +} + +async function fetchWithProxy(url, timeout = 15000) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeout); + + try { + for (const proxy of PROXIES) { + try { + const response = await fetch(buildProxyUrl(proxy, url), { + signal: controller.signal, + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' + } + }); + + if (response.ok) { + const text = await response.text(); + if (text && text.trim().length > 0) { + return text; + } + } + } catch (e) { + // Прокси не сработал — пробуем следующий + } + } + + throw new Error('Все прокси недоступны'); + } finally { + clearTimeout(timer); } - throw new Error('Все прокси недоступны'); +} + +function normalizePosterUrl(poster) { + if (!poster) return ''; + if (poster.startsWith('http')) return poster; + return targetUrl + poster; } function parseColdfilm(html) { @@ -68,12 +98,7 @@ function parseColdfilm(html) { while ((match = posterRegex.exec(html)) !== null) { const poster = match[1]; const alt = match[2].replace('[Смотреть Онлайн]', '').trim(); - - let fullPoster = poster; - if (!fullPoster.startsWith('http')) { - fullPoster = targetUrl + poster; - } - posterMap.set(alt, fullPoster); + posterMap.set(alt, normalizePosterUrl(poster)); } // Альтернативно - title вместо alt @@ -83,11 +108,7 @@ function parseColdfilm(html) { const title = match[2].replace('[Смотреть Онлайн]', '').trim(); if (!posterMap.has(title)) { - let fullPoster = poster; - if (!fullPoster.startsWith('http')) { - fullPoster = targetUrl + poster; - } - posterMap.set(title, fullPoster); + posterMap.set(title, normalizePosterUrl(poster)); } } @@ -192,7 +213,7 @@ async function showQualityModal(filmName) { // Ищем все торренты на странице let match; - const torrentRegex = /href="(\/t\d+\/[^"]+\.torrent)"/gi; + const torrentRegex = /href="(\/t\d+\/[^\"]+\.torrent(?:\?[^\"]*)?)"/gi; while ((match = torrentRegex.exec(filmPageHtml)) !== null) { let url = match[1]; if (!url.startsWith('http')) {