This commit is contained in:
2026-07-22 13:35:36 +03:00
parent b443fa43bf
commit bc8bc9202d
2 changed files with 128 additions and 20 deletions
+87
View File
@@ -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 `<img>` `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.
+41 -20
View File
@@ -14,20 +14,50 @@ const PROXIES = [
'https://thingproxy.freeboard.io/fetch/', 'https://thingproxy.freeboard.io/fetch/',
'https://cors-anywhere.herokuapp.com/', 'https://cors-anywhere.herokuapp.com/',
'https://corsproxy.ca/', 'https://corsproxy.ca/',
'https://proxy.t vot.pw/',
]; ];
// Кеш страниц фильмов и постеров // Кеш страниц фильмов и постеров
const filmData = new Map(); const filmData = new Map();
async function fetchWithProxy(url) { function buildProxyUrl(proxy, url) {
for (const proxy of PROXIES) { return proxy + encodeURIComponent(url);
try { }
const response = await fetch(proxy + encodeURIComponent(url), { timeout: 15000 });
if (response.ok) return await response.text(); async function fetchWithProxy(url, timeout = 15000) {
} catch (e) {} 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) { function parseColdfilm(html) {
@@ -68,12 +98,7 @@ function parseColdfilm(html) {
while ((match = posterRegex.exec(html)) !== null) { while ((match = posterRegex.exec(html)) !== null) {
const poster = match[1]; const poster = match[1];
const alt = match[2].replace('[Смотреть Онлайн]', '').trim(); const alt = match[2].replace('[Смотреть Онлайн]', '').trim();
posterMap.set(alt, normalizePosterUrl(poster));
let fullPoster = poster;
if (!fullPoster.startsWith('http')) {
fullPoster = targetUrl + poster;
}
posterMap.set(alt, fullPoster);
} }
// Альтернативно - title вместо alt // Альтернативно - title вместо alt
@@ -83,11 +108,7 @@ function parseColdfilm(html) {
const title = match[2].replace('[Смотреть Онлайн]', '').trim(); const title = match[2].replace('[Смотреть Онлайн]', '').trim();
if (!posterMap.has(title)) { if (!posterMap.has(title)) {
let fullPoster = poster; posterMap.set(title, normalizePosterUrl(poster));
if (!fullPoster.startsWith('http')) {
fullPoster = targetUrl + poster;
}
posterMap.set(title, fullPoster);
} }
} }
@@ -192,7 +213,7 @@ async function showQualityModal(filmName) {
// Ищем все торренты на странице // Ищем все торренты на странице
let match; let match;
const torrentRegex = /href="(\/t\d+\/[^"]+\.torrent)"/gi; const torrentRegex = /href="(\/t\d+\/[^\"]+\.torrent(?:\?[^\"]*)?)"/gi;
while ((match = torrentRegex.exec(filmPageHtml)) !== null) { while ((match = torrentRegex.exec(filmPageHtml)) !== null) {
let url = match[1]; let url = match[1];
if (!url.startsWith('http')) { if (!url.startsWith('http')) {