Add AniList tracking for downloaded episodes + Add auto skip filler episodes option (#266)

This commit is contained in:
scigward 2026-03-03 16:06:52 +03:00 committed by GitHub
parent 05914ccd36
commit 5e4838d954
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 76 additions and 29 deletions

View file

@ -415,6 +415,8 @@ struct AssetMetadata: Codable {
let seasonNumber: Int? let seasonNumber: Int?
/// Indicates whether this episode is a filler (derived from metadata at download time) /// Indicates whether this episode is a filler (derived from metadata at download time)
let isFiller: Bool? let isFiller: Bool?
let aniListID: Int?
let totalEpisodes: Int?
init( init(
title: String, title: String,
@ -428,7 +430,9 @@ struct AssetMetadata: Codable {
showPosterURL: URL? = nil, showPosterURL: URL? = nil,
episodeTitle: String? = nil, episodeTitle: String? = nil,
seasonNumber: Int? = nil, seasonNumber: Int? = nil,
isFiller: Bool? = nil isFiller: Bool? = nil,
aniListID: Int? = nil,
totalEpisodes: Int? = nil
) { ) {
self.title = title self.title = title
self.overview = overview self.overview = overview
@ -442,6 +446,8 @@ struct AssetMetadata: Codable {
self.episodeTitle = episodeTitle self.episodeTitle = episodeTitle
self.seasonNumber = seasonNumber self.seasonNumber = seasonNumber
self.isFiller = isFiller self.isFiller = isFiller
self.aniListID = aniListID
self.totalEpisodes = totalEpisodes
} }
} }

View file

@ -23,10 +23,11 @@ struct DownloadRequest {
let aniListID: Int? let aniListID: Int?
let malID: Int? let malID: Int?
let isFiller: Bool? let isFiller: Bool?
let totalEpisodes: Int?
init(url: URL, headers: [String: String], title: String? = nil, imageURL: URL? = nil, init(url: URL, headers: [String: String], title: String? = nil, imageURL: URL? = nil,
isEpisode: Bool = false, showTitle: String? = nil, season: Int? = nil, isEpisode: Bool = false, showTitle: String? = nil, season: Int? = nil,
episode: Int? = nil, subtitleURL: URL? = nil, showPosterURL: URL? = nil, aniListID: Int? = nil, malID: Int? = nil, isFiller: Bool? = nil) { episode: Int? = nil, subtitleURL: URL? = nil, showPosterURL: URL? = nil, aniListID: Int? = nil, malID: Int? = nil, isFiller: Bool? = nil, totalEpisodes: Int? = nil) {
self.url = url self.url = url
self.headers = headers self.headers = headers
self.title = title self.title = title
@ -40,6 +41,7 @@ struct DownloadRequest {
self.aniListID = aniListID self.aniListID = aniListID
self.malID = malID self.malID = malID
self.isFiller = isFiller self.isFiller = isFiller
self.totalEpisodes = totalEpisodes
} }
} }
@ -62,6 +64,7 @@ extension JSController {
showTitle: String? = nil, season: Int? = nil, episode: Int? = nil, showTitle: String? = nil, season: Int? = nil, episode: Int? = nil,
subtitleURL: URL? = nil, showPosterURL: URL? = nil, subtitleURL: URL? = nil, showPosterURL: URL? = nil,
aniListID: Int? = nil, malID: Int? = nil, isFiller: Bool? = nil, aniListID: Int? = nil, malID: Int? = nil, isFiller: Bool? = nil,
totalEpisodes: Int? = nil,
completionHandler: ((Bool, String) -> Void)? = nil) { completionHandler: ((Bool, String) -> Void)? = nil) {
@ -69,7 +72,7 @@ extension JSController {
url: url, headers: headers, title: title, imageURL: imageURL, url: url, headers: headers, title: title, imageURL: imageURL,
isEpisode: isEpisode, showTitle: showTitle, season: season, isEpisode: isEpisode, showTitle: showTitle, season: season,
episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL, episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL,
aniListID: aniListID, malID: malID, isFiller: isFiller aniListID: aniListID, malID: malID, isFiller: isFiller, totalEpisodes: totalEpisodes
) )
logDownloadStart(request: request) logDownloadStart(request: request)
@ -114,7 +117,8 @@ extension JSController {
showPosterURL: request.showPosterURL, showPosterURL: request.showPosterURL,
aniListID: request.aniListID, aniListID: request.aniListID,
malID: request.malID, malID: request.malID,
isFiller: request.isFiller isFiller: request.isFiller,
totalEpisodes: request.totalEpisodes
) )
self.downloadWithOriginalMethod(request: qualityRequest, completionHandler: completionHandler) self.downloadWithOriginalMethod(request: qualityRequest, completionHandler: completionHandler)
} else { } else {
@ -142,6 +146,7 @@ extension JSController {
imageURL: URL? = nil, isEpisode: Bool = false, showTitle: String? = nil, imageURL: URL? = nil, isEpisode: Bool = false, showTitle: String? = nil,
season: Int? = nil, episode: Int? = nil, subtitleURL: URL? = nil, season: Int? = nil, episode: Int? = nil, subtitleURL: URL? = nil,
showPosterURL: URL? = nil, aniListID: Int? = nil, malID: Int? = nil, isFiller: Bool? = nil, showPosterURL: URL? = nil, aniListID: Int? = nil, malID: Int? = nil, isFiller: Bool? = nil,
totalEpisodes: Int? = nil,
completionHandler: ((Bool, String) -> Void)? = nil) { completionHandler: ((Bool, String) -> Void)? = nil) {
@ -149,7 +154,7 @@ extension JSController {
url: url, headers: headers, title: title, imageURL: imageURL, url: url, headers: headers, title: title, imageURL: imageURL,
isEpisode: isEpisode, showTitle: showTitle, season: season, isEpisode: isEpisode, showTitle: showTitle, season: season,
episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL, episode: episode, subtitleURL: subtitleURL, showPosterURL: showPosterURL,
aniListID: aniListID, malID: malID, isFiller: isFiller aniListID: aniListID, malID: malID, isFiller: isFiller, totalEpisodes: totalEpisodes
) )
downloadMP4(request: request, completionHandler: completionHandler) downloadMP4(request: request, completionHandler: completionHandler)
@ -382,7 +387,9 @@ extension JSController {
showPosterURL: request.showPosterURL ?? request.imageURL, showPosterURL: request.showPosterURL ?? request.imageURL,
episodeTitle: nil, episodeTitle: nil,
seasonNumber: nil, seasonNumber: nil,
isFiller: request.isFiller isFiller: request.isFiller,
aniListID: request.aniListID,
totalEpisodes: request.totalEpisodes
) )
} }
@ -436,6 +443,7 @@ extension JSController {
aniListID: request.aniListID, aniListID: request.aniListID,
malID: request.malID, malID: request.malID,
isFiller: request.isFiller, isFiller: request.isFiller,
totalEpisodes: request.totalEpisodes,
completionHandler: completionHandler completionHandler: completionHandler
) )
} }

View file

@ -117,6 +117,7 @@ extension JSController {
aniListID: Int? = nil, aniListID: Int? = nil,
malID: Int? = nil, malID: Int? = nil,
isFiller: Bool? = nil, isFiller: Bool? = nil,
totalEpisodes: Int? = nil,
completionHandler: ((Bool, String) -> Void)? = nil completionHandler: ((Bool, String) -> Void)? = nil
) { ) {
// If a module is provided, use the stream type aware download // If a module is provided, use the stream type aware download
@ -137,6 +138,7 @@ extension JSController {
aniListID: aniListID, aniListID: aniListID,
malID: malID, malID: malID,
isFiller: isFiller, isFiller: isFiller,
totalEpisodes: totalEpisodes,
completionHandler: completionHandler completionHandler: completionHandler
) )
return return
@ -163,7 +165,9 @@ extension JSController {
season: season, season: season,
episode: episode, episode: episode,
showPosterURL: showPosterURL, // Main show poster showPosterURL: showPosterURL, // Main show poster
isFiller: isFiller isFiller: isFiller,
aniListID: aniListID,
totalEpisodes: totalEpisodes
) )
// Create the download ID now so we can use it for notifications // Create the download ID now so we can use it for notifications

View file

@ -39,6 +39,7 @@ extension JSController {
aniListID: Int? = nil, aniListID: Int? = nil,
malID: Int? = nil, malID: Int? = nil,
isFiller: Bool? = nil, isFiller: Bool? = nil,
totalEpisodes: Int? = nil,
completionHandler: ((Bool, String) -> Void)? = nil completionHandler: ((Bool, String) -> Void)? = nil
) { ) {
let streamType = module.metadata.streamType.lowercased() let streamType = module.metadata.streamType.lowercased()
@ -59,6 +60,7 @@ extension JSController {
aniListID: aniListID, aniListID: aniListID,
malID: malID, malID: malID,
isFiller: isFiller, isFiller: isFiller,
totalEpisodes: totalEpisodes,
completionHandler: completionHandler completionHandler: completionHandler
) )
}else { }else {
@ -77,6 +79,7 @@ extension JSController {
aniListID: aniListID, aniListID: aniListID,
malID: malID, malID: malID,
isFiller: isFiller, isFiller: isFiller,
totalEpisodes: totalEpisodes,
completionHandler: completionHandler completionHandler: completionHandler
) )
} }

View file

@ -281,8 +281,8 @@ struct DownloadView: View {
} }
}, },
subtitlesURL: asset.localSubtitleURL?.absoluteString, subtitlesURL: asset.localSubtitleURL?.absoluteString,
aniListID: 0, aniListID: asset.metadata?.aniListID ?? 0,
totalEpisodes: asset.metadata?.episode ?? 0, totalEpisodes: asset.metadata?.totalEpisodes ?? 0,
episodeImageUrl: asset.metadata?.posterURL?.absoluteString ?? "", episodeImageUrl: asset.metadata?.posterURL?.absoluteString ?? "",
headers: nil headers: nil
) )
@ -1050,7 +1050,6 @@ struct EnhancedShowEpisodesView: View {
var body: some View { var body: some View {
ZStack { ZStack {
heroImageSection
mainScrollView mainScrollView
.navigationBarHidden(true) .navigationBarHidden(true)
.ignoresSafeArea(.container, edges: .top) .ignoresSafeArea(.container, edges: .top)
@ -1099,7 +1098,10 @@ struct EnhancedShowEpisodesView: View {
@ViewBuilder @ViewBuilder
private var mainScrollView: some View { private var mainScrollView: some View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
contentContainer ZStack(alignment: .top) {
heroImageSection
contentContainer
}
} }
.onAppear { .onAppear {
UIScrollView.appearance().bounces = false UIScrollView.appearance().bounces = false
@ -1108,24 +1110,22 @@ struct EnhancedShowEpisodesView: View {
@ViewBuilder @ViewBuilder
private var heroImageSection: some View { private var heroImageSection: some View {
if let posterURL = group.posterURL { Group {
LazyImage(url: posterURL) { state in if let posterURL = group.posterURL {
if let uiImage = state.imageContainer?.image { LazyImage(url: posterURL) { @MainActor state in
Image(uiImage: uiImage) if let uiImage = state.imageContainer?.image {
.resizable() Image(uiImage: uiImage)
.aspectRatio(contentMode: .fill) .resizable()
} else { .aspectRatio(contentMode: .fill)
placeholderGradient .frame(width: UIScreen.main.bounds.width, height: 700)
.clipped()
} else {
placeholderGradient
}
} }
} else {
placeholderGradient
} }
.ignoresSafeArea(.all)
.frame(maxWidth: .infinity, maxHeight: 400)
.clipped()
} else {
placeholderGradient
.ignoresSafeArea(.all)
.frame(maxWidth: .infinity, maxHeight: 400)
.clipped()
} }
} }
@ -1142,6 +1142,8 @@ struct EnhancedShowEpisodesView: View {
endPoint: .bottomTrailing endPoint: .bottomTrailing
) )
) )
.frame(width: UIScreen.main.bounds.width, height: 700)
.clipped()
} }
@ViewBuilder @ViewBuilder

View file

@ -747,7 +747,8 @@ private extension EpisodeCell {
showPosterURL: showPosterImageURL, showPosterURL: showPosterImageURL,
aniListID: itemID, aniListID: itemID,
malID: malIDFromParent, malID: malIDFromParent,
isFiller: isFiller isFiller: isFiller,
totalEpisodes: totalEpisodes
) { success, message in ) { success, message in
if success { if success {
Logger.shared.log("Started download for Episode \(self.episodeID + 1): \(self.episode)", type: "Download") Logger.shared.log("Started download for Episode \(self.episodeID + 1): \(self.episode)", type: "Download")

View file

@ -1373,7 +1373,21 @@ struct MediaInfoView: View {
return return
} }
let nextEpisode = episodeLinks[currentIndex + 1] let autoSkipFillers = UserDefaults.standard.bool(forKey: "autoSkipFillers")
var nextIndex = currentIndex + 1
if autoSkipFillers, let fillerSet = jikanFillerSet {
while nextIndex < episodeLinks.count, fillerSet.contains(episodeLinks[nextIndex].number) {
Logger.shared.log("Skipping filler episode \(episodeLinks[nextIndex].number)", type: "Debug")
nextIndex += 1
}
guard nextIndex < episodeLinks.count else {
Logger.shared.log("No more non-filler episodes to play", type: "Info")
return
}
}
let nextEpisode = episodeLinks[nextIndex]
selectedEpisodeNumber = nextEpisode.number selectedEpisodeNumber = nextEpisode.number
fetchStream(href: nextEpisode.href) fetchStream(href: nextEpisode.href)
DropManager.shared.showDrop( DropManager.shared.showDrop(

View file

@ -302,6 +302,7 @@ struct SettingsViewBackup: View {
"skipIntroOutroVisible", "skipIntroOutroVisible",
"pipButtonVisible", "pipButtonVisible",
"autoplayNext", "autoplayNext",
"autoSkipFillers",
"videoQualityWiFi", "videoQualityWiFi",
"videoQualityCellular", "videoQualityCellular",
"subtitlesEnabled", "subtitlesEnabled",

View file

@ -258,6 +258,7 @@ struct SettingsViewPlayer: View {
@AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = false @AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = false
@AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true @AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true
@AppStorage("autoplayNext") private var autoplayNext: Bool = true @AppStorage("autoplayNext") private var autoplayNext: Bool = true
@AppStorage("autoSkipFillers") private var autoSkipFillers: Bool = false
@AppStorage("introDBEnabled") private var introDBEnabled: Bool = true @AppStorage("introDBEnabled") private var introDBEnabled: Bool = true
@AppStorage("videoQualityWiFi") private var wifiQuality: String = VideoQualityPreference.defaultWiFiPreference.rawValue @AppStorage("videoQualityWiFi") private var wifiQuality: String = VideoQualityPreference.defaultWiFiPreference.rawValue
@ -301,6 +302,13 @@ struct SettingsViewPlayer: View {
showDivider: true showDivider: true
) )
SettingsToggleRow(
icon: "forward.fill",
title: NSLocalizedString("Auto Skip Filler Episodes", comment: ""),
isOn: $autoSkipFillers,
showDivider: true
)
SettingsTextFieldRow( SettingsTextFieldRow(
icon: "timer", icon: "timer",
title: NSLocalizedString("Completion Percentage", comment: ""), title: NSLocalizedString("Completion Percentage", comment: ""),