Skip to content

Commit

Permalink
Close download from Open Subtitles using REST API, #3920 (#4678)
Browse files Browse the repository at this point in the history
* Close download from Open Subtitles using REST API, #3920

This commit will:
- Add a new OpenSubClient class
- Change the OpenSub class to use the new client
- Change the OpenSub default language from eng to en
- Change name of provider from opensubtitles.org to opensubtitles.com
- Add a new loggedIn property to OnlineSubtitle
- Add a new logout method to OnlineSubtitle
- Add a new iinaLogoutCompleted notification to AppData
- Change AppDelegate to log out of the online subtitle provider during
  termination

This is a temporary stopgap solution that updates the builtin legacy
code in order to meet the end of 2023 deadline imposed by Open Subtitles
for switching to the new REST API. The plan is still to replace all
built-in code for downloading from online subtitle providers with
plug-in implementations.

* Close download from Open Subtitles using REST API, #3920

This commit will:
- Add a new OpenSubClient class
- Change the OpenSub class to use the new client
- Change the OpenSub default language from eng to en
- Change name of provider from opensubtitles.org to opensubtitles.com
- Add a new loggedIn property to OnlineSubtitle
- Add a new logout method to OnlineSubtitle
- Add a new iinaLogoutCompleted notification to AppData
- Change AppDelegate to log out of the online subtitle provider during
  termination

This is a temporary stopgap solution that updates the builtin legacy
code in order to meet the end of 2023 deadline imposed by Open Subtitles
for switching to the new REST API. The plan is still to replace all
built-in code for downloading from online subtitle providers with
plug-in implementations.

* Use "en" by default if all preferred languages are not supported by OpenSubtitle

---------

Co-authored-by: Hechen Li <lhc199652@gmail.com>
  • Loading branch information
low-batt and lhc70000 committed Dec 16, 2023
1 parent 9eecc7a commit eacd64c
Show file tree
Hide file tree
Showing 7 changed files with 1,346 additions and 336 deletions.
4 changes: 4 additions & 0 deletions iina.xcodeproj/project.pbxproj
Expand Up @@ -20,6 +20,7 @@
515B5E5A2A579903001FCD49 /* iina-plugin in Copy Executables */ = {isa = PBXBuildFile; fileRef = E38558872A484A2D0083772D /* iina-plugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5196819A29EC963F00B05D55 /* CoreDisplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5196819929EC963F00B05D55 /* CoreDisplay.framework */; };
519872FF26879B9B00F84BCC /* AccessibilityPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519872FE26879B9B00F84BCC /* AccessibilityPreferences.swift */; };
51AC1CAB2A9FBF3700DF7079 /* OpenSubClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AC1CAA2A9FBF3700DF7079 /* OpenSubClient.swift */; };
51C1BA3A291CA76700C1208A /* InfoDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C1BA39291CA76700C1208A /* InfoDictionary.swift */; };
51CACB9529D500290034CEE5 /* VideoPIPViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CACB9429D500290034CEE5 /* VideoPIPViewController.swift */; };
51DE55C92A6646710050AD06 /* Sysctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DE55C82A6646710050AD06 /* Sysctl.swift */; };
Expand Down Expand Up @@ -898,6 +899,7 @@
5196819929EC963F00B05D55 /* CoreDisplay.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreDisplay.framework; path = /System/Library/Frameworks/CoreDisplay.framework; sourceTree = "<group>"; };
519872FE26879B9B00F84BCC /* AccessibilityPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityPreferences.swift; sourceTree = "<group>"; };
51A0F0F629FA2C8E000130CF /* Beta.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Beta.xcconfig; sourceTree = "<group>"; };
51AC1CAA2A9FBF3700DF7079 /* OpenSubClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSubClient.swift; sourceTree = "<group>"; };
51C1BA39291CA76700C1208A /* InfoDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoDictionary.swift; sourceTree = "<group>"; };
51CACB9429D500290034CEE5 /* VideoPIPViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPIPViewController.swift; sourceTree = "<group>"; };
51DE55C82A6646710050AD06 /* Sysctl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sysctl.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2272,6 +2274,7 @@
840AF5811E732C8F00F4AF92 /* JustXMLRPC.swift */,
E33BA5C5204BD9FE0069A0F6 /* SubChooseViewController.swift */,
1326718020852D0D000FA7E2 /* SubChooseViewController.xib */,
51AC1CAA2A9FBF3700DF7079 /* OpenSubClient.swift */,
);
name = Sub;
sourceTree = "<group>";
Expand Down Expand Up @@ -3003,6 +3006,7 @@
51DE55C92A6646710050AD06 /* Sysctl.swift in Sources */,
845FB0C71D39462E00C011E0 /* ControlBarView.swift in Sources */,
E3513AFB20F120F600F8C347 /* PreferenceViewController.swift in Sources */,
51AC1CAB2A9FBF3700DF7079 /* OpenSubClient.swift in Sources */,
E38BD4AF20054BD9007635FC /* MainWindow.swift in Sources */,
51E63DFB29CFB031008AFC20 /* PlaySlider.swift in Sources */,
84F5D4A01E44F9DB0060A838 /* KeyBindingItem.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions iina/AppData.swift
Expand Up @@ -132,4 +132,5 @@ extension Notification.Name {
static let iinaPlayerStopped = Notification.Name("iinaPlayerStopped")
static let iinaPlayerShutdown = Notification.Name("iinaPlayerShutdown")
static let iinaPlaySliderLoopKnobChanged = Notification.Name("iinaPlaySliderLoopKnobChanged")
static let iinaLogoutCompleted = Notification.Name("iinaLoggedOutOfSubtitleProvider")
}
156 changes: 117 additions & 39 deletions iina/AppDelegate.swift
Expand Up @@ -39,10 +39,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate {
/** The timer for `OpenFileRepeatTime` and `application(_:openFile:)`. */
private var openFileTimer: Timer?

private var allPlayersHaveShutdown = false

private var commandLineStatus = CommandLineStatus()

private var isTerminating = false

/// Longest time to wait for asynchronous shutdown tasks to finish before giving up on waiting and proceeding with termination.
///
/// Ten seconds was chosen to provide plenty of time for termination and yet not be long enough that users start thinking they will
/// need to force quit IINA. As termination may involve logging out of an online subtitles provider it can take a while to complete if
/// the provider is slow to respond to the logout request.
private let terminationTimeout: TimeInterval = 10

// Windows

lazy var openURLWindow: OpenURLWindowController = OpenURLWindowController()
Expand Down Expand Up @@ -393,21 +402,32 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate {
@objc
func shutdownTimedout() {
timedOut = true
Logger.log("Timed out waiting for players to stop and shutdown", level: .warning)
// For debugging list players that have not terminated.
for player in PlayerCore.playerCores {
let label = player.label ?? "unlabeled"
if !player.isStopped {
Logger.log("Player \(label) failed to stop", level: .warning)
} else if !player.isShutdown {
Logger.log("Player \(label) failed to shutdown", level: .warning)
if !allPlayersHaveShutdown {
Logger.log("Timed out waiting for players to stop and shutdown", level: .warning)
// For debugging list players that have not terminated.
for player in PlayerCore.playerCores {
let label = player.label ?? "unlabeled"
if !player.isStopped {
Logger.log("Player \(label) failed to stop", level: .warning)
} else if !player.isShutdown {
Logger.log("Player \(label) failed to shutdown", level: .warning)
}
}
// For debugging purposes we do not remove observers in case players stop or shutdown after
// the timeout has fired as knowing that occurred maybe useful for debugging why the
// termination sequence failed to complete on time.
Logger.log("Not waiting for players to shutdown; proceeding with application termination",
level: .warning)
}
// For debugging purposes we do not remove observers in case players stop or shutdown after
// the timeout has fired as knowing that occurred maybe useful for debugging why the
// termination sequence failed to complete on time.
Logger.log("Not waiting for players to shutdown; proceeding with application termination",
level: .warning)
if OnlineSubtitle.loggedIn {
// The request to log out of the online subtitles provider has not completed. This should not
// occur as the logout request uses a timeout that is shorter than the termination timeout to
// avoid this occurring. Therefore if this message is logged something has gone wrong with the
// shutdown code.
Logger.log("Timed out waiting for log out of online subtitles provider to complete",
level: .warning)
}
Logger.log("Proceeding with application termination due to time out", level: .warning)
// Tell Cocoa to proceed with termination.
NSApp.reply(toApplicationShouldTerminate: true)
}
Expand All @@ -434,6 +454,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate {
}
}

// The first priority was to shutdown any new input from the user. The second priority is to
// send a logout request if logged into an online subtitles provider as that needs time to
// complete.
if OnlineSubtitle.loggedIn {
// Force the logout request to timeout earlier than the overall termination timeout. This
// request taking too long does not represent an error in the shutdown code, whereas the
// intention of the overall termination timeout is to recover from some sort of hold up in the
// shutdown sequence that should not occur.
OnlineSubtitle.logout(timeout: terminationTimeout - 1)
}

// Close all windows. When a player window is closed it will send a stop command to mpv to stop
// playback and unload the file.
Logger.log("Closing all windows")
Expand All @@ -446,37 +477,51 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate {
// player and shutdown was initiated by typing "q" in the player window. That sends a quit
// command directly to mpv causing mpv and the player to shutdown before application
// termination is initiated.
var canTerminateNow = true
allPlayersHaveShutdown = true
for player in PlayerCore.playerCores {
if !player.isShutdown {
canTerminateNow = false
allPlayersHaveShutdown = false
break
}
}
if allPlayersHaveShutdown {
Logger.log("All players have shutdown")
} else {
// Shutdown of player cores involves sending the stop and quit commands to mpv. Even though
// these commands are sent to mpv using the synchronous API mpv executes them asynchronously.
// This requires IINA to wait for mpv to finish executing these commands.
Logger.log("Waiting for players to stop and shutdown")
}

// Usually will have to wait for logout request to complete if logged into an online subtitle
// provider.
var canTerminateNow = allPlayersHaveShutdown
if OnlineSubtitle.loggedIn {
canTerminateNow = false
Logger.log("Waiting for log out of online subtitles provider to complete")
}

// If the user pressed Q and mpv initiated the termination then players will already be
// shutdown and it may be possible to proceed with termination.
if canTerminateNow {
Logger.log("All players have shutdown; proceeding with application termination")
Logger.log("Proceeding with application termination")
// Tell Cocoa that it is ok to immediately proceed with termination.
return .terminateNow
}

// Shutdown of player cores involves sending the stop and quit commands to mpv. Even though
// these commands are sent to mpv using the synchronous API mpv executes them asynchronously.
// This requires IINA to wait for mpv to finish executing these commands.
Logger.log("Waiting for players to stop and shutdown")

// To ensure termination completes and the user is not required to force quit IINA, impose an
// arbitrary timeout that forces termination to complete. The expectation is that this timeout
// is never triggered. If a timeout warning is logged during termination then that needs to be
// investigated.
var timer: Timer
if #available(macOS 10.12, *) {
timer = Timer(timeInterval: 10, repeats: false) { _ in
timer = Timer(timeInterval: terminationTimeout, repeats: false) { _ in
// Once macOS 10.11 is no longer supported the contents of the method can be inlined in this
// closure.
self.shutdownTimedout()
}
} else {
timer = Timer(timeInterval: TimeInterval(10), target: self,
timer = Timer(timeInterval: terminationTimeout, target: self,
selector: #selector(self.shutdownTimedout), userInfo: nil, repeats: false)
}
RunLoop.main.add(timer, forMode: .common)
Expand Down Expand Up @@ -509,6 +554,37 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate {
}
observers.append(observer)

/// Proceed with termination if all outstanding shutdown tasks have completed.
///
/// This method is called when an observer receives a notification that a player has shutdown or an online subtitles provider logout
/// request has completed. If there are no other termination tasks outstanding then this method will instruct AppKit to proceed with
/// termination.
func proceedWithTermination() {
if !allPlayersHaveShutdown {
// If any player has not shutdown then continue waiting.
for player in PlayerCore.playerCores {
guard player.isShutdown else { return }
}
allPlayersHaveShutdown = true
// All players have shutdown.
Logger.log("All players have shutdown")
}
guard !OnlineSubtitle.loggedIn else { return }
// All players have shutdown. No longer logged into an online subtitles provider.
Logger.log("Proceeding with application termination")
// No longer need the timer that forces termination to proceed.
timer.invalidate()
// No longer need the observers for players stopping and shutting down, along with the
// observer for logout requests completing.
ObjcUtils.silenced {
observers.forEach {
NotificationCenter.default.removeObserver($0)
}
}
// Tell AppKit to proceed with termination.
NSApp.reply(toApplicationShouldTerminate: true)
}

// Establish an observer for a player core shutting down.
observer = center.addObserver(forName: .iinaPlayerShutdown, object: nil, queue: .main) { _ in
guard !self.timedOut else {
Expand All @@ -525,22 +601,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate {
Logger.log("Player shutdown after application termination timed out", level: .warning)
return
}
// If any player has not shutdown then continue waiting.
for player in PlayerCore.playerCores {
guard player.isShutdown else { return }
}
// All players have shutdown. Proceed with termination.
Logger.log("All players have shutdown; proceeding with application termination")
// No longer need the timer that forces termination to proceed.
timer.invalidate()
// No longer need the observers for players stopping and shutting down.
ObjcUtils.silenced {
observers.forEach {
NotificationCenter.default.removeObserver($0)
}
proceedWithTermination()
}
observers.append(observer)

// Establish an observer for logging out of the online subtitle provider.
observer = center.addObserver(forName: .iinaLogoutCompleted, object: nil, queue: .main) { _ in
guard !self.timedOut else {
// The request to log out of the online subtitles provider has completed after IINA already
// timed out, gave up waiting for players to shutdown, and told Cocoa to proceed with
// termination. This should not occur as the logout request uses a timeout that is shorter
// than the termination timeout to avoid this occurring. Therefore if this message is logged
// something has gone wrong with the shutdown code.
Logger.log(
"Log out of online subtitles provider completed after application termination timed out",
level: .warning)
return
}
// Tell Cocoa to proceed with termination.
NSApp.reply(toApplicationShouldTerminate: true)
proceedWithTermination()
}
observers.append(observer)

Expand All @@ -551,7 +629,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate {
}
}

// Tell Cocoa that it is ok to proceed with termination, but wait for our reply.
// Tell AppKit that it is ok to proceed with termination, but wait for our reply.
return .terminateLater
}

Expand Down
68 changes: 64 additions & 4 deletions iina/OnlineSubtitle.swift
Expand Up @@ -20,7 +20,9 @@ fileprivate protocol ProviderProtocol {

protocol OnlineSubtitleFetcher {
associatedtype Subtitle: OnlineSubtitle
var loggedIn: Bool { get }
func fetch(from url: URL, withProviderID id: String, playerCore player: PlayerCore) -> Promise<[Subtitle]>
func logout(timeout: TimeInterval?) -> Promise<Void>
}

class OnlineSubtitle: NSObject {
Expand All @@ -33,6 +35,23 @@ class OnlineSubtitle: NSObject {
case fsError
}

static var loggedIn: Bool {
let id = Preference.string(for: .onlineSubProvider) ?? Providers.openSub.id
switch id {
case Providers.openSub.id:
return Providers.openSub.getFetcher().loggedIn
case Providers.shooter.id:
return Providers.shooter.getFetcher().loggedIn
case Providers.assrt.id:
return Providers.assrt.getFetcher().loggedIn
default:
guard let provider = Providers.fromPlugin[id] else {
return Providers.openSub.getFetcher().loggedIn
}
return provider.getFetcher().loggedIn
}
}

/** Prepend a number before file name to avoid overwriting. */
var index: Int

Expand Down Expand Up @@ -60,12 +79,14 @@ class OnlineSubtitle: NSObject {
func getDescription() -> (name: String, left: String, right: String) { return("", "", "") }

class DefaultFetcher {
var loggedIn: Bool { false }
func logout(timeout: TimeInterval?) -> Promise<Void> { .value }
required init() {}
}

class Providers {
static let shooter = Provider<Shooter.Fetcher>(id: ":shooter", name: "shooter.cn")
static let openSub = Provider<OpenSub.Fetcher>(id: ":opensubtitles", name: "opensubtitles.org")
static let openSub = Provider<OpenSub.Fetcher>(id: ":opensubtitles", name: "opensubtitles.com")
static let assrt = Provider<Assrt.Fetcher>(id: ":assrt", name: "assrt.net")

static var fromPlugin: [String: Provider<JSPluginSub.Fetcher>] = [:]
Expand Down Expand Up @@ -135,6 +156,45 @@ class OnlineSubtitle: NSObject {
}
}

static func logout(timeout: TimeInterval? = nil) {
let id = Preference.string(for: .onlineSubProvider) ?? Providers.openSub.id
switch id {
case Providers.openSub.id:
_logout(using: Providers.openSub, timeout: timeout)
case Providers.shooter.id:
_logout(using: Providers.shooter, timeout: timeout)
case Providers.assrt.id:
_logout(using: Providers.assrt, timeout: timeout)
default:
guard let provider = Providers.fromPlugin[id] else {
_logout(using: Providers.openSub, timeout: timeout)
return
}
_logout(using: provider, timeout: timeout)
}
}

fileprivate static func _logout<P: ProviderProtocol>(using provider: P, timeout: TimeInterval? = nil) {
provider.getFetcher().logout(timeout: timeout).catch { err in
let prefix = "Failed to log out of \(provider.name). "
switch err {
case CommonError.cannotConnect(let cause):
log("\(prefix)\(cause.localizedDescription)", level: .error)
case CommonError.networkError(let cause):
let error = cause ?? err
log("\(prefix)\(error.localizedDescription)", level: .error)
case CommonError.timedOut(let cause):
log("\(prefix)\(cause.localizedDescription)", level: .error)
case JSPluginSub.Error.pluginError(let message):
log("\(prefix)\(message)", level: .error)
default:
log("\(prefix)\(err.localizedDescription)", level: .error)
}
}.finally {
NotificationCenter.default.post(Notification(name: .iinaLogoutCompleted, object: self))
}
}

static func search(forFile url: URL, player: PlayerCore, providerID: String? = nil, callback: @escaping ([URL]) -> Void) {
let id = providerID ?? Preference.string(for: .onlineSubProvider) ?? Providers.openSub.id
switch id {
Expand Down Expand Up @@ -177,9 +237,6 @@ class OnlineSubtitle: NSObject {
osdMessage = .networkError
let error = cause ?? err
log("\(prefix)\(error.localizedDescription)", level: .error)
case OpenSub.Error.xmlRpcError(let rpcError):
osdMessage = .networkError
log("\(prefix)\(rpcError.readableDescription)", level: .error)
case CommonError.timedOut(let cause):
osdMessage = .timedOut
log("\(prefix)\(cause.localizedDescription)", level: .error)
Expand All @@ -192,6 +249,9 @@ class OnlineSubtitle: NSObject {
osdMessage = .fileError
log("\(prefix)File is too small. Minimum file size supported by the site is \(minimumFileSize)",
level: .error)
case OpenSub.Error.emptyFile(let reason):
osdMessage = .fileError
log("\(prefix)Invalid file, \(reason)", level: .error)
case OpenSub.Error.loginFailed(let reason):
osdMessage = .cannotLogin
log("\(prefix)Login failed, \(reason)", level: .error)
Expand Down

0 comments on commit eacd64c

Please sign in to comment.