diff --git a/Base.lproj/QueueOptions.xib b/Base.lproj/QueueOptions.xib index 53878e7..30e19a0 100644 --- a/Base.lproj/QueueOptions.xib +++ b/Base.lproj/QueueOptions.xib @@ -16,7 +16,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -772,7 +893,22 @@ - + + + + + + + + + + + + + + + + @@ -799,7 +935,7 @@ - + diff --git a/Classes/DocumentWindowController.swift b/Classes/DocumentWindowController.swift index bcf3934..3579e83 100644 --- a/Classes/DocumentWindowController.swift +++ b/Classes/DocumentWindowController.swift @@ -308,17 +308,7 @@ final class DocumentWindowController: NSWindowController, TracksViewControllerDe } @IBAction func addChaptersEvery(_ sender: NSMenuItem) { - let track: MP42ChapterTrack = mp4.chapters ?? { let track = MP42ChapterTrack(); self.mp4.addTrack(track); return track }() - let minutes = sender.tag * 60 * 1000 - - if minutes > 0 { - for (index, timestamp) in stride(from: 0, to: mp4.duration, by: minutes).enumerated() { - track.addChapter("Chapter \(index + 1)", timestamp: UInt64(timestamp)) - } - } - else { - track.addChapter("Chapter 1", timestamp: 0) - } + mp4.addChapters(everyMinutes: sender.tag) doc.updateChangeCount(.changeDone) tracksViewController.reloadData() diff --git a/Classes/MP42FileAdditions.swift b/Classes/MP42FileAdditions.swift index 1e25644..96c0ff1 100644 --- a/Classes/MP42FileAdditions.swift +++ b/Classes/MP42FileAdditions.swift @@ -18,6 +18,39 @@ private extension String { extension MP42File { + /// Policy for inserting chapter markers when a chapter track already exists. + enum ChaptersInsertPolicy: Int { + /// Clear existing markers, then insert interval chapters. + case replace = 0 + /// Keep existing markers and append interval chapters. + case merge = 1 + } + + /// Adds chapter markers at a fixed interval, matching the document window "Insert a chapter every…" action. + /// - Parameter minutes: Interval in minutes. Values `<= 0` insert a single chapter at the beginning. + /// - Parameter policy: Whether to replace or merge with existing chapter markers. Defaults to `.merge`. + func addChapters(everyMinutes minutes: Int, policy: ChaptersInsertPolicy = .merge) { + let track: MP42ChapterTrack = chapters ?? { + let track = MP42ChapterTrack() + addTrack(track) + return track + }() + + if policy == .replace, track.chapterCount() > 0 { + track.removeChapters(at: IndexSet(integersIn: 0.. 0 { + for (index, timestamp) in stride(from: 0, to: duration, by: interval).enumerated() { + track.addChapter("Chapter \(index + 1)", timestamp: UInt64(timestamp)) + } + } else { + track.addChapter("Chapter 1", timestamp: 0) + } + } + enum TrackHDType : Int { case hd720p = 1 case hd1080p = 2 diff --git a/Classes/QueueAction.swift b/Classes/QueueAction.swift index 72783a0..9cafcaf 100644 --- a/Classes/QueueAction.swift +++ b/Classes/QueueAction.swift @@ -472,8 +472,8 @@ class QueuePrettifyAudioTrackNameAction : NSObject, QueueActionProtocol { class QueueRenameChaptersAction : NSObject, QueueActionProtocol { var type: QueueActionType { return .pre } - var localizedDescription: String { return NSLocalizedString("Prettifying audio track names", comment: "Action localized description") } - override var description: String { return NSLocalizedString("Prettify audio track names", comment: "Action description") } + var localizedDescription: String { return NSLocalizedString("Renaming chapters titles", comment: "Action localized description") } + override var description: String { return NSLocalizedString("Rename chapters titles", comment: "Action description") } override init() {} @@ -496,6 +496,45 @@ class QueueRenameChaptersAction : NSObject, QueueActionProtocol { } +/// An action that inserts chapter markers at a fixed interval. +class QueueAddChaptersAction : NSObject, QueueActionProtocol { + + var type: QueueActionType { return .pre } + var localizedDescription: String { return NSLocalizedString("Adding chapters", comment: "Action localized description") } + override var description: String { return NSLocalizedString("Add chapters", comment: "Action description") } + + let minutes: Int + let policy: MP42File.ChaptersInsertPolicy + + init(minutes: Int, policy: MP42File.ChaptersInsertPolicy = .replace) { + self.minutes = minutes + self.policy = policy + } + + func runAction(_ item: QueueItem) -> Bool { + guard let mp4 = item.mp4File else { return false } + mp4.addChapters(everyMinutes: minutes, policy: policy) + return true + } + + func encode(with aCoder: NSCoder) { + aCoder.encode(Int32(minutes), forKey: "SBQueueAddChaptersActionMinutes") + aCoder.encode(Int32(policy.rawValue), forKey: "SBQueueAddChaptersActionPolicy") + } + + required init?(coder aDecoder: NSCoder) { + self.minutes = Int(aDecoder.decodeInt32(forKey: "SBQueueAddChaptersActionMinutes")) + if aDecoder.containsValue(forKey: "SBQueueAddChaptersActionPolicy") { + self.policy = MP42File.ChaptersInsertPolicy(rawValue: Int(aDecoder.decodeInt32(forKey: "SBQueueAddChaptersActionPolicy"))) ?? .replace + } else { + self.policy = .replace + } + } + + static var supportsSecureCoding: Bool { return true } + +} + enum QueueColorSpaceActionTag: UInt16 { case None = 1 case Rec601PAL diff --git a/Classes/QueueController.swift b/Classes/QueueController.swift index a7fe81f..ec3a098 100644 --- a/Classes/QueueController.swift +++ b/Classes/QueueController.swift @@ -284,9 +284,16 @@ final class QueueController : NSWindowController, NSWindowDelegate, NSPopoverDel if prefs.prettifyAudioTrackName { item.addAction(QueuePrettifyAudioTrackNameAction()) } + // Create chapters before rename/preview so markers exist when previews are generated. + if prefs.addChapters { + let policy = MP42File.ChaptersInsertPolicy(rawValue: prefs.addChaptersPolicy) ?? .replace + item.addAction(QueueAddChaptersAction(minutes: prefs.addChaptersInterval, policy: policy)) + } if prefs.renameChapters { item.addAction(QueueRenameChaptersAction()) } + // Previews when explicitly requested, or whenever the queue is creating chapters. + item.setChaptersPreviewGeneration(prefs.chaptersPreview || prefs.addChapters) if prefs.fixTrackLanguage { item.addAction(QueueSetLanguageAction(language: prefs.fixTrackLanguageValue)) } @@ -563,6 +570,7 @@ final class QueueController : NSWindowController, NSWindowDelegate, NSPopoverDel else { return } if popover == closedPopover { + prefs.saveUserDefaults() popover = nil } if itemPopover == closedPopover { @@ -571,6 +579,7 @@ final class QueueController : NSWindowController, NSWindowDelegate, NSPopoverDel } func windowWillClose(_ notification: Notification) { + prefs.saveUserDefaults() windowController = nil } diff --git a/Classes/QueueItem.swift b/Classes/QueueItem.swift index 3effaf1..ae4decd 100644 --- a/Classes/QueueItem.swift +++ b/Classes/QueueItem.swift @@ -77,11 +77,6 @@ import MP42Foundation attributes[MP4264BitData] = true } - if Prefs.chaptersPreviewTrack { - attributes[MP42GenerateChaptersPreviewTrack] = true - attributes[MP42ChaptersPreviewPosition] = Prefs.chaptersPreviewPosition - } - if Prefs.forceHvc1 { attributes[MP42ForceHvc1] = true } @@ -138,6 +133,31 @@ import MP42Foundation didChangeValue(for: \.actions) } + /// Enables or disables chapter preview image generation using the General preference for position. + func setChaptersPreviewGeneration(_ enabled: Bool) { + queue.sync { + if enabled { + attributes[MP42GenerateChaptersPreviewTrack] = true + attributes[MP42ChaptersPreviewPosition] = Prefs.chaptersPreviewPosition + } else { + attributes.removeValue(forKey: MP42GenerateChaptersPreviewTrack) + attributes.removeValue(forKey: MP42ChaptersPreviewPosition) + } + } + } + + private func updateChaptersPreviewGeneration(for mp4: MP42File) { + let hasChapters = (mp4.chapters?.chapterCount() ?? 0) > 0 + let previewRequested = queue.sync { attributes[MP42GenerateChaptersPreviewTrack] != nil } + let createdChapters = actions.contains { $0 is QueueAddChaptersAction } + + if hasChapters && (previewRequested || createdChapters) { + setChaptersPreviewGeneration(true) + } else { + setChaptersPreviewGeneration(false) + } + } + // MARK: Item processing enum ProcessError: Error { @@ -305,6 +325,11 @@ import MP42Foundation guard let mp4 = mp4File else { return } + // Chapter previews run at the end of write/update. Create-chapters is a pre-action, + // so after prepare() we only keep preview generation when markers actually exist. + // Missing chapters must not fail the queue item. + updateChaptersPreviewGeneration(for: mp4) + #if SB_SANDBOX let mp4Token = MP42SecurityAccessToken(object: mp4) #endif @@ -457,10 +482,11 @@ import MP42Foundation QueueMetadataAction.classForCoder(), QueueSubtitlesAction.classForCoder(), QueueSetLanguageAction.classForCoder(), QueueFixFallbacksAction.classForCoder(), QueueClearTrackNameAction.classForCoder(), QueuePrettifyAudioTrackNameAction.classForCoder(), - QueueRenameChaptersAction.classForCoder(), + QueueRenameChaptersAction.classForCoder(), QueueAddChaptersAction.classForCoder(), QueueOrganizeGroupsAction.classForCoder(), QueueColorSpaceAction.classForCoder(), QueueSetOutputFilenameAction.classForCoder(), QueueClearExistingMetadataAction.classForCoder(), - QueueOptimizeAction.classForCoder(), QueueSendToiTunesAction.classForCoder()], forKey: "SBQueueItemActions") as! [QueueActionProtocol] + QueueOptimizeAction.classForCoder(), QueueSendToiTunesAction.classForCoder(), + QueueChangeAudioLanguageAction.classForCoder(), QueueChangeSubtitleLanguageAction.classForCoder()], forKey: "SBQueueItemActions") as! [QueueActionProtocol] } } diff --git a/Classes/QueuePreferences.swift b/Classes/QueuePreferences.swift index 544e48c..6636601 100644 --- a/Classes/QueuePreferences.swift +++ b/Classes/QueuePreferences.swift @@ -15,6 +15,10 @@ final class QueuePreferences: NSObject { static private let SBQueueClearTrackName: String = "SBQueueClearTrackName" static private let SBQueuePrettifyAudioTrackName: String = "SBQueuePrettifyAudioTrackName" static private let SBQueueRenameChapters: String = "SBQueueRenameChapters" + static private let SBQueueAddChapters: String = "SBQueueAddChapters" + static private let SBQueueAddChaptersInterval: String = "SBQueueAddChaptersInterval" + static private let SBQueueAddChaptersPolicy: String = "SBQueueAddChaptersPolicy" + static private let SBQueueChaptersPreview: String = "SBQueueChaptersPreview" static private let SBQueueMetadata: String = "SBQueueMetadata" static private let SBQueueSubtitles: String = "SBQueueSubtitles" static private let SBQueueSet: String = "SBQueueSet" @@ -62,6 +66,11 @@ final class QueuePreferences: NSObject { @objc dynamic var clearTrackName: Bool @objc dynamic var prettifyAudioTrackName: Bool @objc dynamic var renameChapters: Bool + @objc dynamic var addChapters: Bool + @objc dynamic var addChaptersInterval: Int + /// `MP42File.ChaptersInsertPolicy` raw value: 0 = replace, 1 = merge. + @objc dynamic var addChaptersPolicy: Int + @objc dynamic var chaptersPreview: Bool @objc dynamic var subtitles: Bool @objc dynamic var metadataSet: MetadataPreset? @@ -106,6 +115,10 @@ final class QueuePreferences: NSObject { self.clearTrackName = ud.bool(forKey: QueuePreferences.SBQueueClearTrackName) self.prettifyAudioTrackName = ud.bool(forKey: QueuePreferences.SBQueuePrettifyAudioTrackName) self.renameChapters = ud.bool(forKey: QueuePreferences.SBQueueRenameChapters) + self.addChapters = ud.bool(forKey: QueuePreferences.SBQueueAddChapters) + self.addChaptersInterval = ud.integer(forKey: QueuePreferences.SBQueueAddChaptersInterval) + self.addChaptersPolicy = ud.integer(forKey: QueuePreferences.SBQueueAddChaptersPolicy) + self.chaptersPreview = ud.bool(forKey: QueuePreferences.SBQueueChaptersPreview) self.subtitles = ud.bool(forKey: QueuePreferences.SBQueueSubtitles) if let presetName = ud.string(forKey: QueuePreferences.SBQueueSet) { self.metadataSet = PresetManager.shared.item(name: presetName) as? MetadataPreset @@ -148,6 +161,10 @@ final class QueuePreferences: NSObject { QueuePreferences.SBQueueClearTrackName: false, QueuePreferences.SBQueuePrettifyAudioTrackName: false, QueuePreferences.SBQueueRenameChapters: false, + QueuePreferences.SBQueueAddChapters: false, + QueuePreferences.SBQueueAddChaptersInterval: 5, + QueuePreferences.SBQueueAddChaptersPolicy: 0, + QueuePreferences.SBQueueChaptersPreview: false, QueuePreferences.SBQueueMetadata: false, QueuePreferences.SBQueueSubtitles: true, QueuePreferences.SBQueueApplyColorSpace: false, @@ -188,6 +205,10 @@ final class QueuePreferences: NSObject { ud.set(clearTrackName, forKey: QueuePreferences.SBQueueClearTrackName) ud.set(prettifyAudioTrackName, forKey: QueuePreferences.SBQueuePrettifyAudioTrackName) ud.set(renameChapters, forKey: QueuePreferences.SBQueueRenameChapters) + ud.set(addChapters, forKey: QueuePreferences.SBQueueAddChapters) + ud.set(addChaptersInterval, forKey: QueuePreferences.SBQueueAddChaptersInterval) + ud.set(addChaptersPolicy, forKey: QueuePreferences.SBQueueAddChaptersPolicy) + ud.set(chaptersPreview, forKey: QueuePreferences.SBQueueChaptersPreview) ud.set(subtitles, forKey: QueuePreferences.SBQueueSubtitles) ud.set(metadataSet?.title, forKey: QueuePreferences.SBQueueSet) diff --git a/MP42Foundation b/MP42Foundation index ab93941..c632539 160000 --- a/MP42Foundation +++ b/MP42Foundation @@ -1 +1 @@ -Subproject commit ab939411c392ac068df55178f7b2f932459374db +Subproject commit c63253903d006904502e0e3aec0633ecb8f68484 diff --git a/NEWS.markdown b/NEWS.markdown index 26e4240..6217df6 100644 --- a/NEWS.markdown +++ b/NEWS.markdown @@ -26,7 +26,7 @@ - Added additional keyboard shortcuts [Mock1] - Added support for unofficial metadata often used for Audiobooks - Make it possible to use the services ids in the metadata map -- Fixed the "Insert a chaper at the beginning" menu item action +- Fixed the "Insert a chapter at the beginning" menu item action - Fixed Swedish Ratings - Fixed the "None" artwork option in the Queue @@ -172,7 +172,7 @@ ## Subler 1.6.4 -* Fixes an issue that prevented uncheking tracks in the import sheet. +* Fixes an issue that prevented unchecking tracks in the import sheet. * Fixes an issue that prevented the parsing of some metadata. * Improves Srt export. Subtitles tracks with edit lists are properly exported. * Improved VobSub OCR subtitles timing.