@@ -5,6 +5,7 @@ import WebTorrent from "webtorrent";
import fs from "fs" ;
import path from "path" ;
import mime from "mime-types" ;
import { v2 as webdav } from "webdav-server" ;
import { fileURLToPath } from "url" ;
import { exec , spawn } from "child_process" ;
import crypto from "crypto" ; // 🔒 basit token üretimi için
@@ -28,6 +29,16 @@ const PORT = process.env.PORT || 3001;
const DEBUG _CPU = process . env . DEBUG _CPU === "1" ;
const DISABLE _MEDIA _PROCESSING = process . env . DISABLE _MEDIA _PROCESSING === "1" ;
const AUTO _PAUSE _ON _COMPLETE = process . env . AUTO _PAUSE _ON _COMPLETE === "1" ;
const WEBDAV _ENABLED = [ "1" , "true" , "yes" , "on" ] . includes (
String ( process . env . WEBDAV _ENABLED || "" ) . toLowerCase ( )
) ;
const WEBDAV _USERNAME = process . env . WEBDAV _USERNAME || "" ;
const WEBDAV _PASSWORD = process . env . WEBDAV _PASSWORD || "" ;
const WEBDAV _PATH = process . env . WEBDAV _PATH || "/webdav" ;
const WEBDAV _READONLY = ! [ "0" , "false" , "no" , "off" ] . includes (
String ( process . env . WEBDAV _READONLY || "1" ) . toLowerCase ( )
) ;
const WEBDAV _INDEX _TTL = Number ( process . env . WEBDAV _INDEX _TTL || 60000 ) ;
// --- İndirilen dosyalar için klasör oluştur ---
const DOWNLOAD _DIR = path . join ( _ _dirname , "downloads" ) ;
@@ -52,6 +63,7 @@ const MOVIE_DATA_ROOT = path.join(CACHE_DIR, "movie_data");
const TV _DATA _ROOT = path . join ( CACHE _DIR , "tv_data" ) ;
const ANIME _DATA _ROOT = path . join ( CACHE _DIR , "anime_data" ) ;
const YT _DATA _ROOT = path . join ( CACHE _DIR , "yt_data" ) ;
const WEBDAV _ROOT = path . join ( CACHE _DIR , "webdav" ) ;
const ANIME _ROOT _FOLDER = "_anime" ;
const ROOT _TRASH _REGISTRY = path . join ( CACHE _DIR , "root-trash.json" ) ;
const MUSIC _EXTENSIONS = new Set ( [
@@ -73,7 +85,8 @@ for (const dir of [
MOVIE _DATA _ROOT ,
TV _DATA _ROOT ,
ANIME _DATA _ROOT ,
YT _DATA _ROOT
YT _DATA _ROOT ,
WEBDAV _ROOT
] ) {
if ( ! fs . existsSync ( dir ) ) fs . mkdirSync ( dir , { recursive : true } ) ;
}
@@ -2572,6 +2585,433 @@ function resolveTvDataAbsolute(relPath) {
return resolved ;
}
function sanitizeWebdavSegment ( value ) {
if ( ! value ) return "Unknown" ;
return String ( value )
. replace ( /[\\/:*?"<>|]+/g , "_" )
. replace ( /\s+/g , " " )
. trim ( ) ;
}
function makeUniqueWebdavName ( base , used , suffix = "" ) {
let name = base || "Unknown" ;
if ( ! used . has ( name ) ) {
used . add ( name ) ;
return name ;
}
const fallback = suffix ? ` ${ name } [ ${ suffix } ] ` : ` ${ name } [ ${ used . size } ] ` ;
if ( ! used . has ( fallback ) ) {
used . add ( fallback ) ;
return fallback ;
}
let counter = 2 ;
while ( used . has ( ` ${ fallback } ${ counter } ` ) ) counter += 1 ;
const finalName = ` ${ fallback } ${ counter } ` ;
used . add ( finalName ) ;
return finalName ;
}
function ensureDirSync ( dirPath ) {
if ( ! fs . existsSync ( dirPath ) ) fs . mkdirSync ( dirPath , { recursive : true } ) ;
}
function createSymlinkSafe ( targetPath , linkPath ) {
try {
if ( fs . existsSync ( linkPath ) ) return ;
ensureDirSync ( path . dirname ( linkPath ) ) ;
let stat = null ;
try {
stat = fs . statSync ( targetPath ) ;
} catch ( err ) {
return ;
}
if ( stat ? . isFile ? . ( ) ) {
try {
fs . linkSync ( targetPath , linkPath ) ;
return ;
} catch ( err ) {
if ( err ? . code !== "EXDEV" ) {
// Hardlink başarı sı zsa symlink'e düş
fs . symlinkSync ( targetPath , linkPath ) ;
return ;
}
}
}
fs . symlinkSync ( targetPath , linkPath ) ;
} catch ( err ) {
console . warn ( ` ⚠️ WebDAV link oluşturulamadı ( ${ linkPath } ): ${ err . message } ` ) ;
}
}
function resolveMovieVideoAbsPath ( metadata ) {
const dupe = metadata ? . _dupe || { } ;
const rootFolder = sanitizeRelative ( dupe . folder || "" ) || null ;
let videoPath = dupe . videoPath || metadata ? . videoPath || null ;
if ( ! videoPath ) return null ;
videoPath = String ( videoPath ) . replace ( /\\/g , "/" ) . replace ( /^\/+/ , "" ) ;
const hasRoot = rootFolder && videoPath . startsWith ( ` ${ rootFolder } / ` ) ;
const absPath = hasRoot
? path . join ( DOWNLOAD _DIR , videoPath )
: rootFolder
? path . join ( DOWNLOAD _DIR , rootFolder , videoPath )
: path . join ( DOWNLOAD _DIR , videoPath ) ;
return fs . existsSync ( absPath ) ? absPath : null ;
}
function resolveEpisodeAbsPath ( rootFolder , episode ) {
if ( ! episode ) return null ;
let videoPath = episode . videoPath || episode . file || "" ;
if ( ! videoPath ) return null ;
videoPath = String ( videoPath ) . replace ( /\\/g , "/" ) . replace ( /^\/+/ , "" ) ;
const candidates = [ ] ;
if ( rootFolder === ANIME _ROOT _FOLDER ) {
candidates . push ( path . join ( DOWNLOAD _DIR , videoPath ) ) ;
if ( videoPath . includes ( "/" ) ) {
candidates . push ( path . join ( DOWNLOAD _DIR , path . basename ( videoPath ) ) ) ;
}
} else {
if ( rootFolder && ! videoPath . startsWith ( ` ${ rootFolder } / ` ) ) {
candidates . push ( path . join ( DOWNLOAD _DIR , rootFolder , videoPath ) ) ;
}
candidates . push ( path . join ( DOWNLOAD _DIR , videoPath ) ) ;
}
if ( episode . file && episode . file !== videoPath ) {
const fallbackFile = String ( episode . file )
. replace ( /\\/g , "/" )
. replace ( /^\/+/ , "" ) ;
candidates . push ( path . join ( DOWNLOAD _DIR , fallbackFile ) ) ;
if ( rootFolder && ! fallbackFile . startsWith ( ` ${ rootFolder } / ` ) ) {
candidates . push ( path . join ( DOWNLOAD _DIR , rootFolder , fallbackFile ) ) ;
}
}
for ( const absPath of candidates ) {
if ( fs . existsSync ( absPath ) ) return absPath ;
}
return null ;
}
function rebuildWebdavIndex ( ) {
try {
if ( fs . existsSync ( WEBDAV _ROOT ) ) {
fs . rmSync ( WEBDAV _ROOT , { recursive : true , force : true } ) ;
}
fs . mkdirSync ( WEBDAV _ROOT , { recursive : true } ) ;
} catch ( err ) {
console . warn ( ` ⚠️ WebDAV kökü temizlenemedi ( ${ WEBDAV _ROOT } ): ${ err . message } ` ) ;
return ;
}
const categoryDefs = [
{ label : "Movies" } ,
{ label : "TV Shows" } ,
{ label : "Anime" }
] ;
for ( const cat of categoryDefs ) {
const dir = path . join ( WEBDAV _ROOT , cat . label ) ;
ensureDirSync ( dir ) ;
}
const isVideoExt = ( value ) =>
VIDEO _EXTS . includes ( String ( value || "" ) . toLowerCase ( ) ) ;
const shouldSkip = ( relPath ) => {
if ( ! relPath ) return true ;
const normalized = relPath . replace ( /\\/g , "/" ) . replace ( /^\/+/ , "" ) ;
const segments = normalized . split ( "/" ) . filter ( Boolean ) ;
if ( ! segments . length ) return true ;
return segments . some ( ( seg ) => seg . startsWith ( "yt_" ) ) ;
} ;
const makeEpisodeCode = ( seasonNum , episodeNum ) => {
const season = Number ( seasonNum ) ;
const episode = Number ( episodeNum ) ;
if ( ! Number . isFinite ( season ) || ! Number . isFinite ( episode ) ) return null ;
return ` S ${ String ( season ) . padStart ( 2 , "0" ) } E ${ String ( episode ) . padStart ( 2 , "0" ) } ` ;
} ;
const getEpisodeNumber = ( episodeKey , episode ) => {
const direct =
episode ? . episodeNumber ? ?
episode ? . number ? ?
episode ? . episode ? ?
null ;
if ( Number . isFinite ( Number ( direct ) ) ) return Number ( direct ) ;
const match = String ( episodeKey || "" ) . match ( /(\d{1,4})/ ) ;
return match ? Number ( match [ 1 ] ) : null ;
} ;
// Movies
try {
const movieDirs = fs . readdirSync ( MOVIE _DATA _ROOT , { withFileTypes : true } ) ;
const usedMovieNames = new Set ( ) ;
for ( const dirent of movieDirs ) {
if ( ! dirent . isDirectory ( ) ) continue ;
const metaPath = path . join ( MOVIE _DATA _ROOT , dirent . name , "metadata.json" ) ;
if ( ! fs . existsSync ( metaPath ) ) continue ;
let metadata = null ;
try {
metadata = JSON . parse ( fs . readFileSync ( metaPath , "utf-8" ) ) ;
} catch ( err ) {
continue ;
}
const absVideo = resolveMovieVideoAbsPath ( metadata ) ;
if ( ! absVideo ) continue ;
if ( shouldSkip ( absVideo . replace ( DOWNLOAD _DIR , "" ) ) ) continue ;
const title =
metadata ? . title ||
metadata ? . matched _title ||
metadata ? . _dupe ? . displayName ||
dirent . name ;
const year =
metadata ? . release _date ? . slice ? . ( 0 , 4 ) ||
metadata ? . matched _year ||
metadata ? . year ||
null ;
const baseName = sanitizeWebdavSegment (
year ? ` ${ title } ( ${ year } ) ` : title
) ;
const uniqueName = makeUniqueWebdavName (
baseName ,
usedMovieNames ,
metadata ? . id || dirent . name
) ;
const movieDir = path . join ( WEBDAV _ROOT , "Movies" , uniqueName ) ;
ensureDirSync ( movieDir ) ;
const ext = path . extname ( absVideo ) ;
const fileName = sanitizeWebdavSegment ( ` ${ uniqueName } ${ ext } ` ) ;
const linkPath = path . join ( movieDir , fileName ) ;
createSymlinkSafe ( absVideo , linkPath ) ;
}
} catch ( err ) {
console . warn ( ` ⚠️ WebDAV movie index oluşturulamadı : ${ err . message } ` ) ;
}
const buildSeriesCategory = (
dataRoot ,
categoryLabel ,
usedShowNames ,
coveredRoots
) => {
try {
const dirs = fs . readdirSync ( dataRoot , { withFileTypes : true } ) ;
for ( const dirent of dirs ) {
if ( ! dirent . isDirectory ( ) ) continue ;
const seriesDir = path . join ( dataRoot , dirent . name ) ;
const seriesPath = path . join ( seriesDir , "series.json" ) ;
if ( ! fs . existsSync ( seriesPath ) ) continue ;
let seriesData = null ;
try {
seriesData = JSON . parse ( fs . readFileSync ( seriesPath , "utf-8" ) ) ;
} catch ( err ) {
continue ;
}
const { rootFolder } = parseTvSeriesKey ( dirent . name ) ;
if ( coveredRoots ) coveredRoots . add ( rootFolder ) ;
const showTitle = sanitizeWebdavSegment (
seriesData ? . name || seriesData ? . title || dirent . name
) ;
const uniqueShow = makeUniqueWebdavName (
showTitle ,
usedShowNames ,
seriesData ? . id || dirent . name
) ;
const showDir = path . join ( WEBDAV _ROOT , categoryLabel , uniqueShow ) ;
const seasons = seriesData ? . seasons || { } ;
let createdSeasonCount = 0 ;
for ( const seasonKey of Object . keys ( seasons ) ) {
const season = seasons [ seasonKey ] ;
if ( ! season ? . episodes ) continue ;
const seasonNumber =
season ? . seasonNumber ? ? Number ( seasonKey ) ? ? null ;
const seasonLabel = seasonNumber
? ` Season ${ String ( seasonNumber ) . padStart ( 2 , "0" ) } `
: "Season" ;
const seasonDir = path . join ( showDir , seasonLabel ) ;
let createdEpisodeCount = 0 ;
for ( const episodeKey of Object . keys ( season . episodes ) ) {
const episode = season . episodes [ episodeKey ] ;
const absVideo = resolveEpisodeAbsPath ( rootFolder , episode ) ;
if ( ! absVideo ) continue ;
if ( shouldSkip ( absVideo . replace ( DOWNLOAD _DIR , "" ) ) ) continue ;
const ext = path . extname ( absVideo ) ;
if ( ! isVideoExt ( ext ) ) continue ;
if ( createdEpisodeCount === 0 ) {
ensureDirSync ( seasonDir ) ;
ensureDirSync ( showDir ) ;
createdSeasonCount += 1 ;
}
const episodeNumber = getEpisodeNumber ( episodeKey , episode ) ;
const code = makeEpisodeCode ( seasonNumber , episodeNumber ) ;
const safeCode =
code ||
` S ${ String ( seasonNumber || 0 ) . padStart ( 2 , "0" ) } E ${ String (
Number ( episodeNumber ) || 0
) . padStart ( 2 , "0" ) } ` ;
const fileName = sanitizeWebdavSegment (
` ${ uniqueShow } - ${ safeCode } ${ ext } `
) ;
const linkPath = path . join ( seasonDir , fileName ) ;
createSymlinkSafe ( absVideo , linkPath ) ;
createdEpisodeCount += 1 ;
}
}
if ( createdSeasonCount === 0 && fs . existsSync ( showDir ) ) {
fs . rmSync ( showDir , { recursive : true , force : true } ) ;
}
}
} catch ( err ) {
console . warn ( ` ⚠️ WebDAV ${ categoryLabel } index oluşturulamadı : ${ err . message } ` ) ;
}
} ;
const tvUsedShowNames = new Set ( ) ;
const animeUsedShowNames = new Set ( ) ;
const coveredTvRoots = new Set ( ) ;
buildSeriesCategory ( TV _DATA _ROOT , "TV Shows" , tvUsedShowNames , coveredTvRoots ) ;
buildSeriesCategory ( ANIME _DATA _ROOT , "Anime" , animeUsedShowNames , null ) ;
const buildSeriesFromInfoJson = ( categoryLabel , usedShowNames , coveredRoots ) => {
try {
const roots = fs . readdirSync ( DOWNLOAD _DIR , { withFileTypes : true } ) ;
for ( const dirent of roots ) {
if ( ! dirent . isDirectory ( ) ) continue ;
const rootFolder = dirent . name ;
if ( coveredRoots ? . has ( rootFolder ) ) continue ;
const infoPath = path . join ( DOWNLOAD _DIR , rootFolder , "info.json" ) ;
if ( ! fs . existsSync ( infoPath ) ) continue ;
let info = null ;
try {
info = JSON . parse ( fs . readFileSync ( infoPath , "utf-8" ) ) ;
} catch ( err ) {
continue ;
}
const episodes = info ? . seriesEpisodes ;
if ( ! episodes || typeof episodes !== "object" ) continue ;
const showBuckets = new Map ( ) ;
for ( const [ relPath , episode ] of Object . entries ( episodes ) ) {
if ( ! episode ) continue ;
if ( shouldSkip ( relPath ) ) continue ;
const absVideo = path . join ( DOWNLOAD _DIR , rootFolder , relPath ) ;
if ( ! fs . existsSync ( absVideo ) ) continue ;
const ext = path . extname ( absVideo ) . toLowerCase ( ) ;
if ( ! isVideoExt ( ext ) ) continue ;
const showTitleRaw =
episode . showTitle || info ? . name || rootFolder || "Unknown" ;
const showKey = ` ${ episode . showId || "" } __ ${ showTitleRaw } ` ;
if ( ! showBuckets . has ( showKey ) ) {
showBuckets . set ( showKey , {
title : showTitleRaw ,
episodes : [ ]
} ) ;
}
showBuckets . get ( showKey ) . episodes . push ( {
absVideo ,
relPath ,
season : episode . season ,
episode : episode . episode ,
key : episode . key
} ) ;
}
for ( const bucket of showBuckets . values ( ) ) {
const showTitle = sanitizeWebdavSegment ( bucket . title ) ;
const uniqueShow = makeUniqueWebdavName (
showTitle ,
usedShowNames ,
bucket . title
) ;
const showDir = path . join ( WEBDAV _ROOT , categoryLabel , uniqueShow ) ;
let createdSeasonCount = 0 ;
for ( const entry of bucket . episodes ) {
const seasonNumber = Number ( entry . season ) ;
const episodeNumber = Number ( entry . episode ) ;
const seasonLabel = Number . isFinite ( seasonNumber )
? ` Season ${ String ( seasonNumber ) . padStart ( 2 , "0" ) } `
: "Season" ;
const seasonDir = path . join ( showDir , seasonLabel ) ;
if ( createdSeasonCount === 0 ) {
ensureDirSync ( showDir ) ;
}
if ( ! fs . existsSync ( seasonDir ) ) {
ensureDirSync ( seasonDir ) ;
createdSeasonCount += 1 ;
}
const ext = path . extname ( entry . absVideo ) ;
const code =
entry . key ||
makeEpisodeCode ( seasonNumber , episodeNumber ) ||
` S ${ String ( seasonNumber || 0 ) . padStart ( 2 , "0" ) } E ${ String (
Number ( episodeNumber ) || 0
) . padStart ( 2 , "0" ) } ` ;
const fileName = sanitizeWebdavSegment (
` ${ uniqueShow } - ${ code } ${ ext } `
) ;
const linkPath = path . join ( seasonDir , fileName ) ;
createSymlinkSafe ( entry . absVideo , linkPath ) ;
}
}
}
} catch ( err ) {
console . warn ( ` ⚠️ WebDAV ${ categoryLabel } info.json index oluşturulamadı : ${ err . message } ` ) ;
}
} ;
buildSeriesFromInfoJson ( "TV Shows" , tvUsedShowNames , coveredTvRoots ) ;
}
let webdavIndexLast = 0 ;
let webdavIndexBuilding = false ;
async function ensureWebdavIndexFresh ( ) {
if ( ! WEBDAV _ENABLED ) return ;
const now = Date . now ( ) ;
if ( webdavIndexBuilding ) return ;
if ( now - webdavIndexLast < WEBDAV _INDEX _TTL ) return ;
webdavIndexBuilding = true ;
try {
rebuildWebdavIndex ( ) ;
webdavIndexLast = Date . now ( ) ;
} finally {
webdavIndexBuilding = false ;
}
}
function webdavAuthMiddleware ( req , res , next ) {
if ( ! WEBDAV _ENABLED ) return res . status ( 404 ) . end ( ) ;
const authHeader = req . headers . authorization || "" ;
if ( ! authHeader . startsWith ( "Basic " ) ) {
res . setHeader ( "WWW-Authenticate" , "Basic realm=\"Dupe WebDAV\"" ) ;
return res . status ( 401 ) . end ( ) ;
}
const raw = Buffer . from ( authHeader . slice ( 6 ) , "base64" )
. toString ( "utf-8" )
. split ( ":" ) ;
const user = raw . shift ( ) || "" ;
const pass = raw . join ( ":" ) ;
if ( ! WEBDAV _USERNAME || ! WEBDAV _PASSWORD ) {
return res . status ( 500 ) . end ( ) ;
}
if ( user !== WEBDAV _USERNAME || pass !== WEBDAV _PASSWORD ) {
res . setHeader ( "WWW-Authenticate" , "Basic realm=\"Dupe WebDAV\"" ) ;
return res . status ( 401 ) . end ( ) ;
}
return next ( ) ;
}
function webdavReadonlyGuard ( req , res , next ) {
if ( ! WEBDAV _READONLY ) return next ( ) ;
const allowed = new Set ( [ "GET" , "HEAD" , "OPTIONS" , "PROPFIND" ] ) ;
if ( ! allowed . has ( req . method ) ) {
return res . status ( 403 ) . end ( ) ;
}
return next ( ) ;
}
function serveCachedFile ( req , res , filePath , { maxAgeSeconds = 86400 } = { } ) {
if ( ! fs . existsSync ( filePath ) ) {
return res . status ( 404 ) . send ( "Dosya bulunamadı " ) ;
@@ -4612,6 +5052,316 @@ function moveInfoDataBetweenRoots(oldRoot, newRoot, oldRel, newRel, isDirectory)
return true ;
}
function collectSeriesIdsForPath ( info , oldRel , isDirectory ) {
const ids = new Set ( ) ;
if ( ! info || typeof info !== "object" ) return ids ;
const normalizedOldRel = normalizeTrashPath ( oldRel ) ;
const shouldMatch = ( key ) => {
if ( ! normalizedOldRel ) return true ;
const normalizedKey = normalizeTrashPath ( key ) ;
if ( normalizedKey === normalizedOldRel ) return true ;
return (
isDirectory &&
normalizedOldRel &&
normalizedKey . startsWith ( ` ${ normalizedOldRel } / ` )
) ;
} ;
const episodes = info . seriesEpisodes || { } ;
for ( const [ key , value ] of Object . entries ( episodes ) ) {
if ( ! shouldMatch ( key ) ) continue ;
const id = value ? . showId ? ? value ? . id ? ? null ;
if ( id ) ids . add ( id ) ;
}
const files = info . files || { } ;
for ( const [ key , value ] of Object . entries ( files ) ) {
if ( ! shouldMatch ( key ) ) continue ;
const id = value ? . seriesMatch ? . id ? ? null ;
if ( id ) ids . add ( id ) ;
}
return ids ;
}
function collectMovieRelPathsForMove ( info , oldRel , isDirectory ) {
const relPaths = new Set ( ) ;
if ( ! info || typeof info !== "object" ) return relPaths ;
const normalizedOldRel = normalizeTrashPath ( oldRel ) ;
const shouldMatch = ( key ) => {
if ( ! normalizedOldRel ) return true ;
const normalizedKey = normalizeTrashPath ( key ) ;
if ( normalizedKey === normalizedOldRel ) return true ;
return (
isDirectory &&
normalizedOldRel &&
normalizedKey . startsWith ( ` ${ normalizedOldRel } / ` )
) ;
} ;
if ( info . primaryVideoPath && shouldMatch ( info . primaryVideoPath ) ) {
relPaths . add ( normalizeTrashPath ( info . primaryVideoPath ) ) ;
}
const files = info . files || { } ;
for ( const [ key , value ] of Object . entries ( files ) ) {
if ( ! value ? . movieMatch ) continue ;
if ( ! shouldMatch ( key ) ) continue ;
relPaths . add ( normalizeTrashPath ( key ) ) ;
}
return relPaths ;
}
function mapRelPathForMove ( oldRel , newRel , relPath , isDirectory ) {
const normalizedOldRel = normalizeTrashPath ( oldRel ) ;
const normalizedNewRel = normalizeTrashPath ( newRel ) ;
const normalizedRel = normalizeTrashPath ( relPath ) ;
if ( ! normalizedOldRel ) return normalizedRel ;
if ( normalizedRel === normalizedOldRel ) return normalizedNewRel ;
if (
isDirectory &&
normalizedRel . startsWith ( ` ${ normalizedOldRel } / ` )
) {
const suffix = normalizedRel . slice ( normalizedOldRel . length ) . replace ( /^\/+/ , "" ) ;
return normalizedNewRel
? ` ${ normalizedNewRel } ${ suffix ? ` / ${ suffix } ` : "" } `
: suffix ;
}
return normalizedRel ;
}
function moveMovieDataDir ( oldKey , newKey , oldRoot , newRoot , newRelPath ) {
if ( ! oldKey || ! newKey ) return false ;
if ( oldKey === newKey ) return false ;
const oldDir = movieDataPathsByKey ( oldKey ) . dir ;
const newDir = movieDataPathsByKey ( newKey ) . dir ;
if ( ! fs . existsSync ( oldDir ) ) return false ;
if ( fs . existsSync ( newDir ) ) {
try {
fs . rmSync ( newDir , { recursive : true , force : true } ) ;
} catch ( err ) {
console . warn ( ` ⚠️ Movie metadata hedefi temizlenemedi ( ${ newDir } ): ${ err . message } ` ) ;
}
}
try {
fs . renameSync ( oldDir , newDir ) ;
} catch ( err ) {
try {
fs . cpSync ( oldDir , newDir , { recursive : true } ) ;
fs . rmSync ( oldDir , { recursive : true , force : true } ) ;
} catch ( copyErr ) {
console . warn ( ` ⚠️ Movie metadata taşı namadı ( ${ oldDir } ): ${ copyErr . message } ` ) ;
return false ;
}
}
const metadataPath = path . join ( newDir , "metadata.json" ) ;
if ( fs . existsSync ( metadataPath ) ) {
try {
const metadata = JSON . parse ( fs . readFileSync ( metadataPath , "utf-8" ) ) ;
if ( metadata ? . _dupe ) {
metadata . _dupe . folder = newRoot ;
metadata . _dupe . videoPath = newRelPath ;
metadata . _dupe . cacheKey = newKey ;
}
fs . writeFileSync ( metadataPath , JSON . stringify ( metadata , null , 2 ) , "utf-8" ) ;
} catch ( err ) {
console . warn ( ` ⚠️ movie metadata güncellenemedi ( ${ metadataPath } ): ${ err . message } ` ) ;
}
}
return true ;
}
function moveMovieDataBetweenRoots ( oldRoot , newRoot , oldRel , newRel , relPaths , isDirectory ) {
if ( ! oldRoot || ! newRoot ) return false ;
if ( ! relPaths || relPaths . size === 0 ) return false ;
let movedAny = false ;
for ( const relPath of relPaths ) {
const mappedRel = mapRelPathForMove ( oldRel , newRel , relPath , isDirectory ) ;
const oldKey = movieDataKey ( oldRoot , relPath ) ;
const newKey = movieDataKey ( newRoot , mappedRel ) ;
if ( moveMovieDataDir ( oldKey , newKey , oldRoot , newRoot , mappedRel ) ) {
movedAny = true ;
}
}
return movedAny ;
}
function moveMovieDataWithinRoot ( rootFolder , oldRel , newRel , relPaths , isDirectory ) {
if ( ! rootFolder ) return false ;
if ( ! relPaths || relPaths . size === 0 ) return false ;
let movedAny = false ;
for ( const relPath of relPaths ) {
const mappedRel = mapRelPathForMove ( oldRel , newRel , relPath , isDirectory ) ;
const oldKey = movieDataKey ( rootFolder , relPath ) ;
const newKey = movieDataKey ( rootFolder , mappedRel ) ;
if ( moveMovieDataDir ( oldKey , newKey , rootFolder , rootFolder , mappedRel ) ) {
movedAny = true ;
}
}
return movedAny ;
}
function updateSeriesJsonAfterRootMove ( seriesData , oldRoot , newRoot , oldRel , newRel ) {
if ( ! seriesData || typeof seriesData !== "object" ) return false ;
let changed = false ;
const oldKey = seriesData ? . _dupe ? . key || null ;
if ( seriesData . _dupe ) {
seriesData . _dupe . folder = newRoot ;
seriesData . _dupe . key = tvSeriesKey ( newRoot , seriesData . _dupe . seriesId ) ;
changed = true ;
}
const encodeKey = ( key ) =>
String ( key || "" )
. split ( path . sep )
. map ( encodeURIComponent )
. join ( "/" ) ;
const oldKeyEncoded = oldKey ? encodeKey ( oldKey ) : null ;
const newKeyEncoded = seriesData ? . _dupe ? . key
? encodeKey ( seriesData . _dupe . key )
: null ;
const oldPrefix = oldKeyEncoded ? ` /tv-data/ ${ oldKeyEncoded } / ` : null ;
const newPrefix = newKeyEncoded ? ` /tv-data/ ${ newKeyEncoded } / ` : null ;
const replaceTvDataPath = ( value ) => {
if ( ! value || ! oldPrefix || ! newPrefix || typeof value !== "string" ) {
return value ;
}
if ( value . includes ( oldPrefix ) ) {
changed = true ;
return value . replace ( oldPrefix , newPrefix ) ;
}
return value ;
} ;
if ( seriesData . poster ) seriesData . poster = replaceTvDataPath ( seriesData . poster ) ;
if ( seriesData . backdrop )
seriesData . backdrop = replaceTvDataPath ( seriesData . backdrop ) ;
const oldRelNorm = normalizeTrashPath ( oldRel ) ;
const newRelNorm = normalizeTrashPath ( newRel ) ;
const shouldTransform = ( value ) => {
const normalized = normalizeTrashPath ( value ) ;
if ( ! oldRelNorm ) return true ;
if ( normalized === oldRelNorm ) return true ;
return (
oldRelNorm && normalized . startsWith ( ` ${ oldRelNorm } / ` )
) ;
} ;
const transformRel = ( value ) => {
const normalized = normalizeTrashPath ( value ) ;
if ( ! shouldTransform ( normalized ) ) return value ;
const suffix = oldRelNorm
? normalized . slice ( oldRelNorm . length ) . replace ( /^\/+/ , "" )
: normalized ;
const next = newRelNorm ? ` ${ newRelNorm } ${ suffix ? ` / ${ suffix } ` : "" } ` : suffix ;
if ( next !== value ) changed = true ;
return next ;
} ;
const seasons = seriesData ? . seasons || { } ;
for ( const season of Object . values ( seasons ) ) {
if ( ! season ) continue ;
if ( season . poster ) season . poster = replaceTvDataPath ( season . poster ) ;
if ( ! season . episodes ) continue ;
for ( const episode of Object . values ( season . episodes ) ) {
if ( ! episode ) continue ;
if ( episode . still ) episode . still = replaceTvDataPath ( episode . still ) ;
if ( episode . file ) {
const nextFile = transformRel ( episode . file ) ;
if ( nextFile !== episode . file ) {
episode . file = nextFile ;
changed = true ;
}
}
if ( episode . videoPath ) {
const video = String ( episode . videoPath ) . replace ( /\\/g , "/" ) ;
if ( video . startsWith ( ` ${ oldRoot } / ` ) ) {
episode . videoPath = ` ${ newRoot } / ${ video . slice ( oldRoot . length + 1 ) } ` ;
changed = true ;
} else {
const nextVideo = transformRel ( video ) ;
if ( nextVideo !== video ) {
episode . videoPath = nextVideo ;
changed = true ;
}
}
}
}
}
return changed ;
}
function moveSeriesDataBetweenRoots ( oldRoot , newRoot , oldRel , newRel , seriesIds ) {
if ( ! oldRoot || ! newRoot ) return false ;
if ( ! seriesIds || ! seriesIds . size ) return false ;
let movedAny = false ;
for ( const seriesId of seriesIds ) {
if ( ! seriesId ) continue ;
const oldPaths = tvSeriesPaths ( oldRoot , seriesId ) ;
if ( ! oldPaths || ! fs . existsSync ( oldPaths . dir ) ) continue ;
const newKey = tvSeriesKey ( newRoot , seriesId ) ;
if ( ! newKey ) continue ;
const newPaths = tvSeriesPathsByKey ( newKey ) ;
if ( ! newPaths ) continue ;
if ( fs . existsSync ( newPaths . dir ) ) {
try {
fs . rmSync ( newPaths . dir , { recursive : true , force : true } ) ;
} catch ( err ) {
console . warn ( ` ⚠️ TV metadata hedefi temizlenemedi ( ${ newPaths . dir } ): ${ err . message } ` ) ;
}
}
try {
fs . renameSync ( oldPaths . dir , newPaths . dir ) ;
} catch ( err ) {
try {
fs . cpSync ( oldPaths . dir , newPaths . dir , { recursive : true } ) ;
fs . rmSync ( oldPaths . dir , { recursive : true , force : true } ) ;
} catch ( copyErr ) {
console . warn ( ` ⚠️ TV metadata taşı namadı ( ${ oldPaths . dir } ): ${ copyErr . message } ` ) ;
continue ;
}
}
const metadataPath = path . join ( newPaths . dir , "series.json" ) ;
if ( fs . existsSync ( metadataPath ) ) {
try {
const seriesData = JSON . parse ( fs . readFileSync ( metadataPath , "utf-8" ) ) ;
const changed = updateSeriesJsonAfterRootMove (
seriesData ,
oldRoot ,
newRoot ,
oldRel ,
newRel
) ;
if ( changed ) {
fs . writeFileSync (
metadataPath ,
JSON . stringify ( seriesData , null , 2 ) ,
"utf-8"
) ;
}
} catch ( err ) {
console . warn ( ` ⚠️ series.json güncellenemedi ( ${ metadataPath } ): ${ err . message } ` ) ;
}
}
movedAny = true ;
}
if ( movedAny ) {
renameSeriesDataPaths ( newRoot , oldRel , newRel ) ;
}
return movedAny ;
}
function renameInfoPaths ( rootFolder , oldRel , newRel ) {
if ( ! rootFolder ) return ;
const info = readInfoForRoot ( rootFolder ) ;
@@ -6161,6 +6911,18 @@ app.post("/api/file/move", requireAuth, (req, res) => {
. json ( { error : "Kök klasör bu yöntemle taşı namaz" } ) ;
}
const preMoveInfo = sourceRoot ? readInfoForRoot ( sourceRoot ) : null ;
const affectedSeriesIds = collectSeriesIdsForPath (
preMoveInfo ,
sourceRelWithinRoot ,
isDirectory
) ;
const affectedMovieRelPaths = collectMovieRelPathsForMove (
preMoveInfo ,
sourceRelWithinRoot ,
isDirectory
) ;
fs . renameSync ( sourceFullPath , newFullPath ) ;
const sameRoot =
@@ -6172,6 +6934,15 @@ app.post("/api/file/move", requireAuth, (req, res) => {
renameInfoPaths ( sourceRoot , sourceRelWithinRoot , destRelWithinRoot ) ;
renameSeriesDataPaths ( sourceRoot , sourceRelWithinRoot , destRelWithinRoot ) ;
renameTrashEntries ( sourceRoot , sourceRelWithinRoot , destRelWithinRoot ) ;
if ( affectedMovieRelPaths . size ) {
moveMovieDataWithinRoot (
sourceRoot ,
sourceRelWithinRoot ,
destRelWithinRoot ,
affectedMovieRelPaths ,
isDirectory
) ;
}
if ( isDirectory ) {
removeThumbnailsForDirectory ( sourceRoot , sourceRelWithinRoot ) ;
} else {
@@ -6187,6 +6958,25 @@ app.post("/api/file/move", requireAuth, (req, res) => {
destRelWithinRoot ,
isDirectory
) ;
if ( affectedSeriesIds . size ) {
moveSeriesDataBetweenRoots (
sourceRoot ,
destRoot ,
sourceRelWithinRoot ,
destRelWithinRoot ,
affectedSeriesIds
) ;
}
if ( affectedMovieRelPaths . size ) {
moveMovieDataBetweenRoots (
sourceRoot ,
destRoot ,
sourceRelWithinRoot ,
destRelWithinRoot ,
affectedMovieRelPaths ,
isDirectory
) ;
}
if ( isDirectory ) {
removeThumbnailsForDirectory ( sourceRoot , sourceRelWithinRoot ) ;
} else {
@@ -6775,7 +7565,7 @@ app.get("/api/movies", requireAuth, (req, res) => {
. readdirSync ( MOVIE _DATA _ROOT , { withFileTypes : true } )
. filter ( ( d ) => d . isDirectory ( ) ) ;
const m ovies = entries
const rawM ovies = entries
. map ( ( dirent ) => {
const key = dirent . name ;
const paths = movieDataPathsByKey ( key ) ;
@@ -6797,6 +7587,17 @@ app.get("/api/movies", requireAuth, (req, res) => {
const dupe = metadata . _dupe || { } ;
const rootFolder = dupe . folder || key ;
const videoPath = dupe . videoPath || metadata . videoPath || null ;
const absVideo = resolveMovieVideoAbsPath ( metadata ) ;
if ( ! absVideo ) {
try {
fs . rmSync ( paths . dir , { recursive : true , force : true } ) ;
} catch ( err ) {
console . warn (
` ⚠️ Movie metadata temizlenemedi ( ${ paths . dir } ): ${ err . message } `
) ;
}
return null ;
}
const encodedKey = key
. split ( path . sep )
. map ( encodeURIComponent )
@@ -6815,6 +7616,14 @@ app.get("/api/movies", requireAuth, (req, res) => {
: null ) ;
const cacheKey = paths . key ;
return {
_absVideo : absVideo ,
_cacheDir : paths . dir ,
_cacheKey : cacheKey ,
_dedupeKey :
typeof metadata . id === "number" && metadata . id
? ` id: ${ metadata . id } `
: ` title: ${ ( metadata . title || metadata . matched _title || rootFolder )
. toLowerCase ( ) } - ${ year || "unknown" } ` ,
folder : rootFolder ,
cacheKey ,
id :
@@ -6849,6 +7658,51 @@ app.get("/api/movies", requireAuth, (req, res) => {
} )
. filter ( Boolean ) ;
const dedupedMap = new Map ( ) ;
for ( const item of rawMovies ) {
const key = item . _dedupeKey ;
if ( ! dedupedMap . has ( key ) ) {
dedupedMap . set ( key , item ) ;
continue ;
}
const existing = dedupedMap . get ( key ) ;
const existingScore =
existing ? . metadata ? . _dupe ? . fetchedAt ||
existing ? . metadata ? . _dupe ? . matchedAt ||
existing ? . metadata ? . matchedAt ||
0 ;
const nextScore =
item ? . metadata ? . _dupe ? . fetchedAt ||
item ? . metadata ? . _dupe ? . matchedAt ||
item ? . metadata ? . matchedAt ||
0 ;
if ( nextScore > existingScore ) {
if ( existing ? . _cacheDir ) {
try {
fs . rmSync ( existing . _cacheDir , { recursive : true , force : true } ) ;
} catch ( err ) {
console . warn (
` ⚠️ Movie metadata temizlenemedi ( ${ existing . _cacheDir } ): ${ err . message } `
) ;
}
}
dedupedMap . set ( key , item ) ;
} else if ( item ? . _cacheDir ) {
try {
fs . rmSync ( item . _cacheDir , { recursive : true , force : true } ) ;
} catch ( err ) {
console . warn (
` ⚠️ Movie metadata temizlenemedi ( ${ item . _cacheDir } ): ${ err . message } `
) ;
}
}
}
const movies = Array . from ( dedupedMap . values ( ) ) . map ( ( item ) => {
const { _absVideo , _cacheDir , _cacheKey , _dedupeKey , ... rest } = item ;
return rest ;
} ) ;
movies . sort ( ( a , b ) => {
const yearA = a . year || 0 ;
const yearB = b . year || 0 ;
@@ -8479,6 +9333,35 @@ if (restored.length) {
console . log ( ` ♻️ ${ restored . length } torrent yeniden eklendi. ` ) ;
}
// --- 📁 WebDAV (Infuse) ---
if ( WEBDAV _ENABLED ) {
const webdavServer = new webdav . WebDAVServer ( {
strictMode : false
} ) ;
const webdavBasePath = WEBDAV _PATH . startsWith ( "/" )
? WEBDAV _PATH
: ` / ${ WEBDAV _PATH } ` ;
const userManager = new webdav . SimpleUserManager ( ) ;
if ( WEBDAV _USERNAME && WEBDAV _PASSWORD ) {
userManager . addUser ( WEBDAV _USERNAME , WEBDAV _PASSWORD , false ) ;
}
webdavServer . httpAuthentication = new webdav . HTTPBasicAuthentication (
userManager ,
"Dupe WebDAV"
) ;
webdavServer . setFileSystem ( "/" , new webdav . PhysicalFileSystem ( WEBDAV _ROOT ) ) ;
app . use (
webdavBasePath ,
webdavAuthMiddleware ,
webdavReadonlyGuard ,
async ( req , res ) => {
await ensureWebdavIndexFresh ( ) ;
webdavServer . executeRequest ( req , res ) ;
}
) ;
console . log ( ` 📁 WebDAV aktif: ${ webdavBasePath } ` ) ;
}
// --- ✅ Client build (frontend) dosyaları nı sun ---
const publicDir = path . join ( _ _dirname , "public" ) ;