mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
feat(portals): find the same movie in your other playlists
A movie that exists in several imported Xtream playlists now shows a "Sources N" chip on its detail page and in the player. Switching playlist mid-film keeps the timecode, a preferred source can be pinned per movie, and a failed stream offers the alternatives instead of a dead end. The governing rule is that a guess is never presented as a fact. Every metadata value carries where it came from — `api` (the provider said so), `parsed` (inferred from the title) or `probe` (we contacted the stream). Facts render as plain tags, guesses are prefixed `~` in a warning colour, and an unknown value renders no tag at all plus a "check" affordance. Ranking and failover read through `factualOnly()`, so a filename claiming 4K is structurally unable to outrank a source that was actually reached. A probe that could not complete reports "unknown", never "unavailable". Scope is deliberately narrow: Xtream to Xtream, movies only, Electron only. Stalker never reaches the `content` table and M3U is a JSON blob whose search forces live content; both are additive later, since the candidate type already carries all three portal kinds. In the PWA every entry point is gated off and the chip renders nothing. Auto-failover is opt-in and off by default. Each source is tried at most once per session, so it terminates structurally, and the switch is never silent — the toast names the new playlist, offers an undo, and warns that the dub may differ only when both sides state an audio track as fact. Notable details: - Playlist names are routinely the pasted URL, credentials included. They are never rendered raw; a short host-only label is derived instead. - Quality is derived from pixel width, not height: a 2.39:1 1080p master is 1920x800, and bucketing that by height would publish "720p" as a fact. - Switching is a single `inlinePlayback.set()` so the player and engine survive and re-seek; the carried position is read before the 15s persistence throttle so it does not rewind. - Sources from one playlist collapse into a group, since the same film often appears there several times under different stream ids. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2300bea11
commit
004cb65fe6
101 files changed
+7183
-192
No files matched your search
@@ -301,6 +301,18 @@ const CREATE_TABLE_STATEMENTS = [
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_epg_channel_mappings_playlist ON epg_channel_mappings(playlist_id)`,
|
||||
// VOD multi-source pins — the per-movie preferred playlist. Keyed by a
|
||||
// portal-agnostic match key, since the same film has a different provider
|
||||
// id in every portal.
|
||||
`CREATE TABLE IF NOT EXISTS vod_source_pins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
match_key TEXT NOT NULL UNIQUE,
|
||||
playlist_id TEXT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
|
||||
content_id INTEGER NOT NULL,
|
||||
portal_type TEXT NOT NULL CHECK (portal_type IN ('xtream', 'stalker', 'm3u')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_vod_source_pins_playlist ON vod_source_pins(playlist_id)`,
|
||||
// Playback Positions table
|
||||
`CREATE TABLE IF NOT EXISTS playback_positions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
// Manual EPG-to-channel mappings live in their own schema module; re-export
|
||||
// them so `import * as schema from './schema'` keeps a complete namespace.
|
||||
export * from './epg-mapping.schema';
|
||||
export * from './vod-source-pins.schema';
|
||||
|
||||
// Playlists table
|
||||
export const playlists = sqliteTable('playlists', {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Drizzle ORM schema for VOD multi-source pins — the user's per-movie choice
|
||||
* of which playlist a film should play from.
|
||||
*
|
||||
* Split out of schema.ts to keep that file within the repository's
|
||||
* max-lines budget; re-exported from there so `import * as schema`
|
||||
* consumers keep seeing a single schema namespace.
|
||||
*/
|
||||
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
// Per-movie preferred source. Keyed by a portal-agnostic match key
|
||||
// (`tmdb:{id}` or `title:{base}:{year}`) rather than by a provider id,
|
||||
// because the same film has a different id in every portal.
|
||||
//
|
||||
// The playlist foreign key with ON DELETE CASCADE lives in the raw
|
||||
// CREATE TABLE in connection.ts — which is what actually creates this table —
|
||||
// rather than in a Drizzle `.references()`. Declaring it here would mean
|
||||
// importing `playlists` from schema.ts, which re-exports this module, and that
|
||||
// import cycle is exactly what `epg-mapping.schema.ts` avoids too.
|
||||
export const vodSourcePins = sqliteTable(
|
||||
'vod_source_pins',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
matchKey: text('match_key').notNull().unique(),
|
||||
playlistId: text('playlist_id').notNull(),
|
||||
// Provider-side id within that playlist (Xtream `stream_id`).
|
||||
contentId: integer('content_id').notNull(),
|
||||
portalType: text('portal_type', {
|
||||
enum: ['xtream', 'stalker', 'm3u'],
|
||||
}).notNull(),
|
||||
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
},
|
||||
(table) => ({
|
||||
playlistIdx: index('idx_vod_source_pins_playlist').on(table.playlistId),
|
||||
})
|
||||
);
|
||||
|
||||
export type VodSourcePinRow = typeof vodSourcePins.$inferSelect;
|
||||
export type NewVodSourcePinRow = typeof vodSourcePins.$inferInsert;
|
||||
Reference in new issue
Block a user