This commit is contained in:
50/50 2025-07-30 19:54:04 +02:00
parent a1cc56adc0
commit 6aad8c85e8
9 changed files with 132 additions and 41 deletions

View file

@ -20,4 +20,6 @@ struct ContinueWatchingItem: Codable, Identifiable {
let module: ScrapingModule
let headers: [String:String]?
let totalEpisodes: Int
let episodeTitle: String?
let seasonNumber: Int?
}

View file

@ -254,11 +254,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private var isMenuOpen = false
private var menuProtectionTimer: Timer?
let episodeTitle: String
init(module: ScrapingModule,
urlString: String,
fullUrl: String,
title: String,
episodeNumber: Int,
episodeTitle: String,
seasonNumber: Int,
onWatchNext: @escaping () -> Void,
subtitlesURL: String?,
aniListID: Int,
@ -271,6 +275,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self.titleText = title
self.episodeNumber = episodeNumber
self.episodeImageUrl = episodeImageUrl
self.episodeTitle = episodeTitle
self.seasonNumber = seasonNumber
self.onWatchNext = onWatchNext
self.subtitlesURL = subtitlesURL
self.aniListID = aniListID
@ -1257,9 +1263,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
titleContainer.translatesAutoresizingMaskIntoConstraints = false
titleContainer.backgroundColor = .clear
controlsContainerView.addSubview(titleContainer)
episodeNumberLabel = UILabel()
episodeNumberLabel.text = "Episode \(episodeNumber)"
let hasTitle = !episodeTitle.isEmpty
let isSingleSeason = (seasonNumber == 1 || seasonNumber == nil)
let episodePart = "E\(episodeNumber)"
let seasonPart = isSingleSeason ? "" : "S\(seasonNumber ?? 1)"
let colon = hasTitle ? ":" : ""
let main = [seasonPart, episodePart].filter { !$0.isEmpty }.joined()
episodeNumberLabel.text = hasTitle ? "\(main)\(colon) \(episodeTitle)" : main
episodeNumberLabel.textColor = UIColor(white: 1.0, alpha: 0.6)
episodeNumberLabel.font = UIFont.systemFont(ofSize: 14, weight: .semibold)
episodeNumberLabel.textAlignment = .left
@ -1946,7 +1957,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
aniListID: self.aniListID,
module: self.module,
headers: self.headers,
totalEpisodes: self.totalEpisodes
totalEpisodes: self.totalEpisodes,
episodeTitle: self.episodeTitle,
seasonNumber: self.seasonNumber
)
ContinueWatchingManager.shared.save(item: item)
}

View file

@ -314,7 +314,9 @@ class VideoPlayerViewController: UIViewController {
aniListID: self.aniListID,
module: self.module,
headers: self.headers,
totalEpisodes: self.totalEpisodes
totalEpisodes: self.totalEpisodes,
episodeTitle: "",
seasonNumber: self.seasonNumber
)
ContinueWatchingManager.shared.save(item: item)
}

View file

@ -411,6 +411,8 @@ struct AssetMetadata: Codable {
let season: Int?
let episode: Int?
let showPosterURL: URL? // Main show poster URL (distinct from episode-specific images)
let episodeTitle: String?
let seasonNumber: Int?
init(
title: String,
@ -421,7 +423,9 @@ struct AssetMetadata: Codable {
showTitle: String? = nil,
season: Int? = nil,
episode: Int? = nil,
showPosterURL: URL? = nil
showPosterURL: URL? = nil,
episodeTitle: String? = nil,
seasonNumber: Int? = nil
) {
self.title = title
self.overview = overview
@ -432,6 +436,8 @@ struct AssetMetadata: Codable {
self.season = season
self.episode = episode
self.showPosterURL = showPosterURL
self.episodeTitle = episodeTitle
self.seasonNumber = seasonNumber
}
}

View file

@ -51,7 +51,8 @@ extension JSController {
let episodesResult = fetchEpisodesFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
for episodeData in episodesResult {
if let num = episodeData["number"], let link = episodeData["href"], let number = Int(num) {
episodeLinks.append(EpisodeLink(number: number, title: "", href: link, duration: nil))
let title = episodeData["title"] ?? ""
episodeLinks.append(EpisodeLink(number: number, title: title, href: link, duration: nil))
}
}
}

View file

@ -248,6 +248,8 @@ struct DownloadView: View {
fullUrl: asset.originalURL.absoluteString,
title: asset.metadata?.showTitle ?? asset.name,
episodeNumber: asset.metadata?.episode ?? 0,
episodeTitle: asset.metadata?.episodeTitle ?? "",
seasonNumber: asset.metadata?.seasonNumber ?? 1,
onWatchNext: {},
subtitlesURL: asset.localSubtitleURL?.absoluteString,
aniListID: 0,

View file

@ -339,6 +339,8 @@ struct FullWidthContinueWatchingCell: View {
fullUrl: item.fullUrl,
title: item.mediaTitle,
episodeNumber: item.episodeNumber,
episodeTitle: item.episodeTitle ?? "",
seasonNumber: item.seasonNumber ?? 1,
onWatchNext: { },
subtitlesURL: item.subtitles,
aniListID: item.aniListID ?? 0,
@ -405,12 +407,12 @@ struct FullWidthContinueWatchingCell: View {
.lineLimit(1)
HStack {
Text("Episode \(item.episodeNumber)")
Text(episodeLabel(for: item))
.font(.subheadline)
.foregroundColor(.white.opacity(0.9))
.lineLimit(2)
.truncationMode(.tail)
Spacer()
Text("\(Int(item.progress * 100))% seen")
.font(.caption)
.foregroundColor(.white.opacity(0.9))
@ -473,4 +475,15 @@ struct FullWidthContinueWatchingCell: View {
currentProgress = max(0, min(item.progress, 1))
}
}
}
private func episodeLabel(for item: ContinueWatchingItem) -> String {
let hasTitle = !(item.episodeTitle?.isEmpty ?? true)
let isSingleSeason = (item.seasonNumber ?? 1) <= 1
let episodePart = "E\(item.episodeNumber)"
let seasonPart = isSingleSeason ? "" : "S\(item.seasonNumber ?? 1)"
let colon = hasTitle ? ":" : ""
let title = item.episodeTitle ?? ""
let main = [seasonPart, episodePart].filter { !$0.isEmpty }.joined()
return hasTitle ? "\(main)\(colon) \(title)" : main
}

View file

@ -411,6 +411,8 @@ struct ContinueWatchingCell: View {
fullUrl: item.fullUrl,
title: item.mediaTitle,
episodeNumber: item.episodeNumber,
episodeTitle: item.episodeTitle ?? "",
seasonNumber: item.seasonNumber ?? 1,
onWatchNext: { },
subtitlesURL: item.subtitles,
aniListID: item.aniListID ?? 0,
@ -453,12 +455,12 @@ struct ContinueWatchingCell: View {
.lineLimit(1)
HStack {
Text("Episode \(item.episodeNumber)")
Text(episodeLabel(for: item))
.font(.subheadline)
.foregroundColor(.white.opacity(0.9))
.lineLimit(2)
.truncationMode(.tail)
Spacer()
Text("\(Int(item.progress * 100))% seen")
.font(.caption)
.foregroundColor(.white.opacity(0.9))
@ -562,6 +564,16 @@ struct ContinueWatchingCell: View {
}
}
private func episodeLabel(for item: ContinueWatchingItem) -> String {
let hasTitle = !(item.episodeTitle?.isEmpty ?? true)
let isSingleSeason = (item.seasonNumber ?? 1) <= 1
let episodePart = "E\(item.episodeNumber)"
let seasonPart = isSingleSeason ? "" : "S\(item.seasonNumber ?? 1)"
let colon = hasTitle ? ":" : ""
let title = item.episodeTitle ?? ""
let main = [seasonPart, episodePart].filter { !$0.isEmpty }.joined()
return hasTitle ? "\(main)\(colon) \(title)" : main
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity

View file

@ -72,6 +72,8 @@ struct MediaInfoView: View {
@State private var refreshTrigger: Bool = false
@State private var buttonRefreshTrigger: Bool = false
@State private var episodeTitleCache: [Int: String] = [:]
private var selectedRangeKey: String { "selectedRangeStart_\(href)" }
private var selectedSeasonKey: String { "selectedSeason_\(href)" }
@ -1946,38 +1948,63 @@ struct MediaInfoView: View {
DropManager.shared.showDrop(title: "Error", subtitle: "Invalid stream URL", duration: 2.0, icon: UIImage(systemName: "xmark.circle"))
return
}
guard self.activeFetchID == fetchID else { return }
let isMovie = tmdbType == .movie
let customMediaPlayer = CustomMediaPlayerViewController(
module: module,
urlString: url.absoluteString,
fullUrl: fullURL,
title: title,
episodeNumber: selectedEpisodeNumber,
onWatchNext: { selectNextEpisode() },
subtitlesURL: subtitles,
aniListID: itemID ?? 0,
totalEpisodes: episodeLinks.count,
episodeImageUrl: selectedEpisodeImage,
headers: headers ?? nil
)
customMediaPlayer.seasonNumber = selectedSeason + 1
customMediaPlayer.tmdbID = tmdbID
customMediaPlayer.isMovie = isMovie
customMediaPlayer.modalPresentationStyle = .fullScreen
Logger.shared.log("Opening custom media player with url: \(url)")
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
} else {
Logger.shared.log("Failed to find root view controller", type: "Error")
DropManager.shared.showDrop(title: "Error", subtitle: "Failed to present player", duration: 2.0, icon: UIImage(systemName: "xmark.circle"))
let episode: EpisodeLink? = {
if isGroupedBySeasons {
let seasons = groupedEpisodes()
if selectedSeason < seasons.count {
return seasons[selectedSeason].first(where: { $0.number == selectedEpisodeNumber })
}
return nil
} else {
return episodeLinks.first(where: { $0.number == selectedEpisodeNumber })
}
}()
fetchTMDBEpisodeTitle(episodeNumber: selectedEpisodeNumber, season: selectedSeason + 1) { episodeTitle in
let customMediaPlayer = CustomMediaPlayerViewController(
module: module,
urlString: url.absoluteString,
fullUrl: fullURL,
title: title,
episodeNumber: selectedEpisodeNumber,
episodeTitle: episodeTitle,
seasonNumber: selectedSeason + 1,
onWatchNext: { selectNextEpisode() },
subtitlesURL: subtitles,
aniListID: itemID ?? 0,
totalEpisodes: episodeLinks.count,
episodeImageUrl: selectedEpisodeImage,
headers: headers ?? nil
)
customMediaPlayer.tmdbID = tmdbID
customMediaPlayer.isMovie = isMovie
customMediaPlayer.modalPresentationStyle = .fullScreen
Logger.shared.log("Opening custom media player with url: \(url)")
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
} else {
Logger.shared.log("Failed to find root view controller", type: "Error")
DropManager.shared.showDrop(title: "Error", subtitle: "Failed to present player", duration: 2.0, icon: UIImage(systemName: "xmark.circle"))
}
}
}
private func fetchTMDBEpisodeTitle(episodeNumber: Int, season: Int, completion: @escaping (String) -> Void) {
guard let tmdbID = tmdbID else { completion(""); return }
let urlString = "https://api.themoviedb.org/3/tv/\(tmdbID)/season/\(season)/episode/\(episodeNumber)?api_key=738b4edd0a156cc126dc4a4b8aea4aca"
guard let url = URL(string: urlString) else { completion(""); return }
URLSession.shared.dataTask(with: url) { data, _, _ in
var title = ""
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
title = json["name"] as? String ?? ""
}
DispatchQueue.main.async { completion(title) }
}.resume()
}
private func downloadSingleEpisodeDirectly(episode: EpisodeLink) {
if isSingleEpisodeDownloading { return }
@ -2200,8 +2227,14 @@ struct MediaInfoView: View {
completion(nil as EpisodeMetadataInfo?)
return
}
fetchEpisodeMetadataFromNetwork(anilistId: anilistId, episodeNumber: episode.number, completion: completion)
fetchEpisodeMetadataFromNetwork(anilistId: anilistId, episodeNumber: episode.number) { info in
if let info = info, let enTitle = info.title["en"] {
DispatchQueue.main.async {
episodeTitleCache[episode.number] = enTitle
}
}
completion(info)
}
}
private func fetchEpisodeMetadataFromNetwork(anilistId: Int, episodeNumber: Int, completion: @escaping (EpisodeMetadataInfo?) -> Void) {
@ -2390,4 +2423,11 @@ struct MediaInfoView: View {
private var episodeRanges: [Range<Int>] {
generateRanges(for: currentEpisodeList.count)
}
private func getEpisodeTitleForPlayer(episodeNumber: Int) -> String {
if let cached = episodeTitleCache[episodeNumber], !cached.isEmpty {
return cached
}
return ""
}
}