diff --git a/frontend/src/assets/service-amazon.webp b/frontend/src/assets/service-amazon.webp new file mode 100644 index 0000000..779ffde Binary files /dev/null and b/frontend/src/assets/service-amazon.webp differ diff --git a/frontend/src/assets/service-apple-tv.webp b/frontend/src/assets/service-apple-tv.webp new file mode 100644 index 0000000..9de86a1 Binary files /dev/null and b/frontend/src/assets/service-apple-tv.webp differ diff --git a/frontend/src/assets/service-hbo-max.webp b/frontend/src/assets/service-hbo-max.webp new file mode 100644 index 0000000..fd28e55 Binary files /dev/null and b/frontend/src/assets/service-hbo-max.webp differ diff --git a/frontend/src/assets/service-hulu.webp b/frontend/src/assets/service-hulu.webp new file mode 100644 index 0000000..951af47 Binary files /dev/null and b/frontend/src/assets/service-hulu.webp differ diff --git a/frontend/src/assets/service-netflix.webp b/frontend/src/assets/service-netflix.webp new file mode 100644 index 0000000..22f929e Binary files /dev/null and b/frontend/src/assets/service-netflix.webp differ diff --git a/frontend/src/components/ReleaseVersionCard.vue b/frontend/src/components/ReleaseVersionCard.vue index 77ca912..2a44cca 100644 --- a/frontend/src/components/ReleaseVersionCard.vue +++ b/frontend/src/components/ReleaseVersionCard.vue @@ -18,6 +18,13 @@
{{ torrent.resolution }} + {{ displayQualityBadge }} {{ displayCodecBadge }} HDR @@ -77,6 +84,11 @@ import DolbyBadges from './DolbyBadges.vue'; import { buildLanguageFlags } from '../utils/languageFlags'; import blurayLogoUrl from '../assets/bluray.webp'; import dvdLogoUrl from '../assets/dvd.webp'; +import amazonLogoUrl from '../assets/service-amazon.webp'; +import appleTvLogoUrl from '../assets/service-apple-tv.webp'; +import netflixLogoUrl from '../assets/service-netflix.webp'; +import hboMaxLogoUrl from '../assets/service-hbo-max.webp'; +import huluLogoUrl from '../assets/service-hulu.webp'; defineOptions({ inheritAttrs: false, @@ -116,6 +128,15 @@ const hdrPattern = /\bhdr\b|smpte\s*2084|bt\s*2020|hlg/i; const blurayTagPattern = /\bblu[\s.-]*ray\b/i; const blurayPlayablePattern = /(?:^|[\\/])(movieobject|index)\.bdmv$/i; const dvdPlayablePattern = /(?:^|[\\/])video_ts\.ifo$/i; +const webQualityPattern = /^web(?:[ .-]?dl|[ .-]?rip)$/i; + +const serviceLogoMap: Array<{ aliases: string[]; src: string; alt: string }> = [ + { aliases: ['amazon studios', 'amazon prime video', 'prime video', 'amazon', 'amzn'], src: amazonLogoUrl, alt: 'Amazon Prime Video' }, + { aliases: ['apple tv+', 'apple tv plus', 'apple tv', 'atvp'], src: appleTvLogoUrl, alt: 'Apple TV+' }, + { aliases: ['netflix', 'nf', 'nflx'], src: netflixLogoUrl, alt: 'Netflix' }, + { aliases: ['hbo max', 'max', 'hmax'], src: hboMaxLogoUrl, alt: 'HBO Max' }, + { aliases: ['hulu'], src: huluLogoUrl, alt: 'Hulu' }, +]; function hasDolbyTag(value: string | null | undefined): boolean { return Boolean(value && dolbyTagPattern.test(value)); @@ -137,6 +158,10 @@ function hasHdrTag(value: string | null | undefined): boolean { return Boolean(value && hdrPattern.test(value)); } +function normalizeProviderName(value: string | null | undefined): string { + return (value || '').toLowerCase().replace(/[^a-z0-9+]+/g, ' ').trim(); +} + const hasDolbyVision = computed(() => { return ( props.torrent.has_dolby_vision === true @@ -158,12 +183,6 @@ const hasHdr = computed(() => { ); }); -const isDisc = computed(() => { - if (!props.torrent.playable_file) return false; - const filename = props.torrent.playable_file; - return blurayPlayablePattern.test(filename) || dvdPlayablePattern.test(filename) || filename.toLowerCase().endsWith('.iso'); -}); - const isBlurayDisc = computed(() => { if (!props.torrent.playable_file) return false; return blurayPlayablePattern.test(props.torrent.playable_file); @@ -182,8 +201,21 @@ const showDvdLogo = computed(() => { return isDvdDisc.value; }); +const streamingServiceLogo = computed(() => { + if (!props.torrent.quality || !webQualityPattern.test(props.torrent.quality)) { + return null; + } + + const network = normalizeProviderName(props.torrent.network); + if (!network) return null; + + const found = serviceLogoMap.find((entry) => entry.aliases.includes(network)); + return found ? { src: found.src, alt: found.alt } : null; +}); + const displayQualityBadge = computed(() => { if (!props.torrent.quality || hasDolbyTag(props.torrent.quality)) return null; + if (streamingServiceLogo.value) return null; if (blurayTagPattern.test(props.torrent.quality)) return null; return props.torrent.quality; }); @@ -324,6 +356,14 @@ function handleActivate(event: MouseEvent | KeyboardEvent) { text-transform: uppercase; } +.v-service-logo { + align-self: center; + width: auto; + height: 16px; + object-fit: contain; + margin-right: 2px; +} + .v-badge.res { background: #1d4ed8; color: #eff6ff; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 02efb73..70e3750 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -45,6 +45,7 @@ export interface Torrent { playable_file: string | null; resolution: string | null; quality: string | null; + network: string | null; codec: string | null; audio: string | null; audio_languages: string[] | null; diff --git a/mediahive/hivescan/indexer.py b/mediahive/hivescan/indexer.py index 539e3ec..0ae1ecf 100644 --- a/mediahive/hivescan/indexer.py +++ b/mediahive/hivescan/indexer.py @@ -71,6 +71,7 @@ async def _build_torrent_info( playable_file=make_relative_path(playable_file, media_root), resolution=(probe_info.resolution if probe_info else None) or item.resolution, quality=item.quality, + network=item.network, codec=item.codec, audio=item.audio, audio_languages=probe_info.audio_languages if probe_info else None, @@ -159,6 +160,7 @@ async def _collect_episode_files( "has_dolby_atmos": probe.has_dolby_atmos, "resolution": item.resolution, "quality": item.quality, + "network": item.network, "codec": item.codec, "audio": item.audio, "encoder": item.encoder, @@ -206,6 +208,7 @@ async def _collect_episode_files( "has_dolby_atmos": probe.has_dolby_atmos, "resolution": item.resolution, "quality": item.quality, + "network": item.network, "codec": item.codec, "audio": item.audio, "encoder": item.encoder, @@ -265,6 +268,7 @@ def _build_episodes_data( playable_file=make_relative_path(f["path"], media_root), resolution=f.get("probed_resolution") or f.get("resolution"), quality=f.get("quality"), + network=f.get("network"), codec=f.get("codec"), audio=f.get("audio"), audio_languages=f.get("audio_languages"), diff --git a/mediahive/hivescan/models.py b/mediahive/hivescan/models.py index ced825c..a3140aa 100644 --- a/mediahive/hivescan/models.py +++ b/mediahive/hivescan/models.py @@ -41,6 +41,7 @@ class ParsedContent: year: Optional[int] = None resolution: Optional[str] = None quality: Optional[str] = None + network: Optional[str] = None codec: Optional[str] = None audio: Optional[str] = None season: Optional[int] = None diff --git a/mediahive/hivescan/parsing.py b/mediahive/hivescan/parsing.py index 952efba..0f996f1 100644 --- a/mediahive/hivescan/parsing.py +++ b/mediahive/hivescan/parsing.py @@ -39,6 +39,7 @@ async def parse_download(path: Path) -> ParsedContent: year=parsed.get("year"), resolution=normalize_resolution_label(parsed.get("resolution")), quality=parsed.get("quality"), + network=parsed.get("network"), codec=parsed.get("codec"), audio=parsed.get("audio"), season=parsed.get("season"), diff --git a/mediahive/models/data.py b/mediahive/models/data.py index 7242060..c9c8b40 100644 --- a/mediahive/models/data.py +++ b/mediahive/models/data.py @@ -22,6 +22,7 @@ class Torrent(msgspec.Struct): playable_file: str | None = None resolution: str | None = None quality: str | None = None + network: str | None = None codec: str | None = None audio: str | None = None audio_languages: list[str] | None = None