This commit is contained in:
@@ -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.
|
||||||
@@ -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) {
|
||||||
|
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) {
|
for (const proxy of PROXIES) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(proxy + encodeURIComponent(url), { timeout: 15000 });
|
const response = await fetch(buildProxyUrl(proxy, url), {
|
||||||
if (response.ok) return await response.text();
|
signal: controller.signal,
|
||||||
} catch (e) {}
|
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('Все прокси недоступны');
|
throw new Error('Все прокси недоступны');
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user