mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
cute
This commit is contained in:
parent
a1cc56adc0
commit
6aad8c85e8
9 changed files with 132 additions and 41 deletions
|
|
@ -20,4 +20,6 @@ struct ContinueWatchingItem: Codable, Identifiable {
|
|||
let module: ScrapingModule
|
||||
let headers: [String:String]?
|
||||
let totalEpisodes: Int
|
||||
let episodeTitle: String?
|
||||
let seasonNumber: Int?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue