From 0930edfa0f3043d638a306ce8cbdf9a738f1bf67 Mon Sep 17 00:00:00 2001 From: szbk Date: Tue, 28 Oct 2025 18:56:07 +0300 Subject: [PATCH] =?UTF-8?q?UI=20De=C4=9Fi=C5=9Fiklikleri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/routes/TvShows.svelte | 25 ++++- docker-compose.yml | 1 + server/server.js | 152 ++++++++++++++++++++++++++----- 3 files changed, 151 insertions(+), 27 deletions(-) diff --git a/client/src/routes/TvShows.svelte b/client/src/routes/TvShows.svelte index 82f73d9..e5f7bb3 100644 --- a/client/src/routes/TvShows.svelte +++ b/client/src/routes/TvShows.svelte @@ -1158,12 +1158,27 @@ async function openVideoAtIndex(index) { display: flex; gap: 12px; overflow-x: auto; - padding: 10px 2px 14px; - scrollbar-width: thin; + overflow-y: hidden; + padding: 12px; + scrollbar-width: none; /* Scroll bar'ı tamamen gizle */ + -ms-overflow-style: none; /* IE ve Edge için */ background: rgba(0, 0, 0, 0.25); border-radius: 14px; min-height: 80px; } + + /* Scroll bar'ı webkit tarayıcılar için gizle */ + .season-picker::-webkit-scrollbar { + display: none; + } + + .season-picker::-webkit-scrollbar-track { + display: none; + } + + .season-picker::-webkit-scrollbar-thumb { + display: none; + } .season-picker button { background: rgba(255, 255, 255, 0.06); @@ -1183,7 +1198,7 @@ async function openVideoAtIndex(index) { } .season-picker button.selected { - background: #fdce45; + background: #f5b333; color: #101010; border-color: rgba(0, 0, 0, 0.15); } @@ -1583,7 +1598,7 @@ async function openVideoAtIndex(index) { .season-picker { gap: 8px; - padding: 8px 2px 12px; + padding: 12px; min-height: 70px; } @@ -1671,7 +1686,7 @@ async function openVideoAtIndex(index) { .season-picker { gap: 6px; - padding: 6px 1px 10px; + padding: 10px; min-height: 64px; } diff --git a/docker-compose.yml b/docker-compose.yml index 20f4d44..74393ea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,4 +16,5 @@ services: PASSWORD: ${PASSWORD} TMDB_API_KEY: ${TMDB_API_KEY} TVDB_API_KEY: ${TVDB_API_KEY} + FANART_TV_API_KEY: ${FANART_TV_API_KEY} VIDEO_THUMBNAIL_TIME: ${VIDEO_THUMBNAIL_TIME} diff --git a/server/server.js b/server/server.js index 4fa0778..b92dff4 100644 --- a/server/server.js +++ b/server/server.js @@ -57,6 +57,8 @@ const TVDB_USER_TOKEN = "mock_api_key" const TVDB_BASE_URL = "https://api4.thetvdb.com/v4"; const TVDB_IMAGE_BASE = process.env.TVDB_IMAGE_BASE || "https://artworks.thetvdb.com"; +const FANART_TV_API_KEY = process.env.FANART_TV_API_KEY || null; +const FANART_TV_BASE_URL = "https://webservice.fanart.tv/v3"; const FFPROBE_PATH = process.env.FFPROBE_PATH || "ffprobe"; const FFPROBE_MAX_BUFFER = Number(process.env.FFPROBE_MAX_BUFFER) > 0 @@ -107,6 +109,40 @@ async function downloadTvdbImage(imagePath, targetPath) { } } +async function fetchFanartTvImages(thetvdbId) { + if (!FANART_TV_API_KEY || !thetvdbId) return null; + const url = `${FANART_TV_BASE_URL}/tv/${thetvdbId}?api_key=${FANART_TV_API_KEY}`; + + try { + const resp = await fetch(url); + if (!resp.ok) { + console.warn(`⚠️ Fanart.tv isteği başarısız (${url}): ${resp.status}`); + return null; + } + const data = await resp.json(); + console.log("🖼️ Fanart.tv backdrop araması:", { thetvdbId, hasShowbackground: Boolean(data.showbackground) }); + return data; + } catch (err) { + console.warn(`⚠️ Fanart.tv isteği hatası (${url}): ${err.message}`); + return null; + } +} + +async function downloadFanartTvImage(imageUrl, targetPath) { + if (!imageUrl) return false; + try { + const resp = await fetch(imageUrl); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + ensureDirForFile(targetPath); + const arr = await resp.arrayBuffer(); + fs.writeFileSync(targetPath, Buffer.from(arr)); + return true; + } catch (err) { + console.warn(`⚠️ Fanart.tv görsel indirilemedi (${imageUrl}): ${err.message}`); + return false; + } +} + function titleCase(value) { if (!value) return ""; return value @@ -218,6 +254,64 @@ function normalizeTvdbSeason(raw) { }; } +function toDimension(value) { + const num = Number(value); + return Number.isFinite(num) ? num : null; +} + +function artworkKind(value) { + return String(value || "").toLowerCase(); +} + +function isBackgroundArtwork(entry) { + const candidates = [ + entry?.type, + entry?.artworkType, + entry?.artworkTypeSlug, + entry?.type2, + entry?.name, + entry?.artwork + ]; + return candidates + .map(artworkKind) + .some((kind) => + kind.includes("background") || + kind.includes("fanart") || + kind.includes("landscape") || + kind === "1" + ); +} + +function selectBackgroundArtwork(entries) { + if (!Array.isArray(entries) || !entries.length) return null; + const candidates = entries.filter(isBackgroundArtwork); + if (!candidates.length) return null; + + const normalized = candidates.map((entry) => { + const width = toDimension(entry.width); + const height = toDimension(entry.height); + const area = width && height ? width * height : null; + return { entry, width, height, area }; + }); + + const landscape = normalized + .filter((item) => item.width && item.height && item.width >= item.height) + .sort((a, b) => (b.area || 0) - (a.area || 0)); + if (landscape.length) return landscape[0].entry; + + normalized.sort((a, b) => (b.area || 0) - (a.area || 0)); + return normalized[0].entry; +} + +async function fetchTvdbArtworks(seriesId, typeSlug) { + if (!seriesId || !typeSlug) return []; + const resp = await tvdbFetch(`/series/${seriesId}/artworks/${typeSlug}`); + if (!resp) return []; + if (Array.isArray(resp.data)) return resp.data; + if (Array.isArray(resp.artworks)) return resp.artworks; + return []; +} + function infoFilePath(savePath) { return path.join(savePath, INFO_FILENAME); } @@ -1546,20 +1640,36 @@ async function ensureSeriesData( return type.includes("poster") || type === "series" || type === "2"; }) || artworksRaw[0]; - const backdropArtwork = artworksRaw.find((a) => { - const type = String( - a?.type || - a?.artworkType || - a?.type2 || - a?.name || - a?.artwork - ).toLowerCase(); - return ( - type.includes("fanart") || - type.includes("background") || - type === "1" - ); - }); + // Fanart.tv'den backdrop al + let backdropImage = null; + const fanartData = await fetchFanartTvImages(seriesData.id); + if (fanartData?.showbackground && Array.isArray(fanartData.showbackground) && fanartData.showbackground.length > 0) { + // İlk showbackground resmini al + const firstBackdrop = fanartData.showbackground[0]; + backdropImage = firstBackdrop.url; + } + + // Eğer Fanart.tv'de backdrop yoksa, TVDB'i fallback olarak kullan + if (!backdropImage) { + let backdropArtwork = selectBackgroundArtwork(artworksRaw); + if (!backdropArtwork) { + const backgroundArtworks = await fetchTvdbArtworks(seriesData.id, "background"); + backdropArtwork = selectBackgroundArtwork(backgroundArtworks); + } + if (!backdropArtwork) { + const fanartArtworks = await fetchTvdbArtworks(seriesData.id, "fanart"); + backdropArtwork = selectBackgroundArtwork(fanartArtworks); + } + + if (backdropArtwork) { + backdropImage = backdropArtwork?.image || + backdropArtwork?.file || + backdropArtwork?.fileName || + backdropArtwork?.thumbnail || + backdropArtwork?.url || + null; + } + } const posterImage = posterArtwork?.image || @@ -1568,13 +1678,6 @@ async function ensureSeriesData( posterArtwork?.thumbnail || posterArtwork?.url || null; - const backdropImage = - backdropArtwork?.image || - backdropArtwork?.file || - backdropArtwork?.fileName || - backdropArtwork?.thumbnail || - backdropArtwork?.url || - null; const posterPath = path.join(showDir, "poster.jpg"); const backdropPath = path.join(showDir, "backdrop.jpg"); @@ -1583,7 +1686,12 @@ async function ensureSeriesData( await downloadTvdbImage(posterImage, posterPath); } if (backdropImage && !fs.existsSync(backdropPath)) { - await downloadTvdbImage(backdropImage, backdropPath); + // Eğer backdropImage Fanart.tv'den geldiyse, onun indirme fonksiyonunu kullan + if (fanartData?.showbackground && backdropImage.startsWith('http')) { + await downloadFanartTvImage(backdropImage, backdropPath); + } else { + await downloadTvdbImage(backdropImage, backdropPath); + } } const seasonPaths = seasonAssetPaths(rootFolder, seriesInfo.season);