diff --git a/IINA+.xcodeproj/project.pbxproj b/IINA+.xcodeproj/project.pbxproj index 45cf341c..226fd8bf 100644 --- a/IINA+.xcodeproj/project.pbxproj +++ b/IINA+.xcodeproj/project.pbxproj @@ -89,6 +89,8 @@ 01B53B6427B64FD20051F7B4 /* flvplayer.css in Copy Web Files */ = {isa = PBXBuildFile; fileRef = 0120932827A3B441002C7FD3 /* flvplayer.css */; }; 01B53B6527B64FD20051F7B4 /* flvplayer.htm in Copy Web Files */ = {isa = PBXBuildFile; fileRef = 0120932A27A3B441002C7FD3 /* flvplayer.htm */; }; 01B53B6627B64FD20051F7B4 /* flvplayer.js in Copy Web Files */ = {isa = PBXBuildFile; fileRef = 0120932927A3B441002C7FD3 /* flvplayer.js */; }; + 01B82B3728EEF74800928F61 /* SelectVideoCollectionViewHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 01B82B3628EEF74800928F61 /* SelectVideoCollectionViewHeader.xib */; }; + 01B82B3928EEF9D300928F61 /* SelectVideoCollectionViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B82B3828EEF9D300928F61 /* SelectVideoCollectionViewHeader.swift */; }; 01C0807027BB5CF300E87A8C /* JSPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 01C0807227BB5CF300E87A8C /* JSPlayer.storyboard */; }; 01C0807527BB5CFD00E87A8C /* Preferences.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 01C0807727BB5CFD00E87A8C /* Preferences.storyboard */; }; 01C0807C27BC00EA00E87A8C /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 01C0807B27BC00EA00E87A8C /* Alamofire */; }; @@ -102,6 +104,7 @@ 01C0809727BC074000E87A8C /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 01C0809627BC074000E87A8C /* SDWebImage */; }; 01C0809A27BC079200E87A8C /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 01C0809927BC079200E87A8C /* Sparkle */; }; 01C0809D27BC0BC400E87A8C /* SocketRocket in Frameworks */ = {isa = PBXBuildFile; productRef = 01C0809C27BC0BC400E87A8C /* SocketRocket */; }; + 01C338B528E47B3F004CC0B8 /* MBGA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C338B428E47B3F004CC0B8 /* MBGA.swift */; }; 01C373592429F2C2006778D1 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 01C373582429F2C2006778D1 /* README.md */; }; 01CEA84A264255EF0021A645 /* FontSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CEA849264255EF0021A645 /* FontSelectorViewController.swift */; }; 01D8762927D459C5001140DD /* crypto-js.js in Resources */ = {isa = PBXBuildFile; fileRef = 01D8762827D459C5001140DD /* crypto-js.js */; }; @@ -232,10 +235,13 @@ 01B53B5827B64F270051F7B4 /* flv.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = flv.min.js; path = "IINA+/WebFiles/node_modules/flv.js/dist/flv.min.js"; sourceTree = SOURCE_ROOT; }; 01B53B5A27B64F370051F7B4 /* CommentCoreLibrary.min.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = CommentCoreLibrary.min.js; path = "IINA+/WebFiles/node_modules/comment-core-library/dist/CommentCoreLibrary.min.js"; sourceTree = SOURCE_ROOT; }; 01B53B5C27B64F420051F7B4 /* style.min.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; name = style.min.css; path = "IINA+/WebFiles/node_modules/comment-core-library/dist/css/style.min.css"; sourceTree = SOURCE_ROOT; }; + 01B82B3628EEF74800928F61 /* SelectVideoCollectionViewHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SelectVideoCollectionViewHeader.xib; sourceTree = ""; }; + 01B82B3828EEF9D300928F61 /* SelectVideoCollectionViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectVideoCollectionViewHeader.swift; sourceTree = ""; }; 01C0807127BB5CF300E87A8C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/JSPlayer.storyboard; sourceTree = ""; }; 01C0807427BB5CF500E87A8C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/JSPlayer.strings"; sourceTree = ""; }; 01C0807627BB5CFD00E87A8C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Preferences.storyboard; sourceTree = ""; }; 01C0807927BB5CFF00E87A8C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Preferences.strings"; sourceTree = ""; }; + 01C338B428E47B3F004CC0B8 /* MBGA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MBGA.swift; sourceTree = ""; }; 01C373582429F2C2006778D1 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; 01CEA849264255EF0021A645 /* FontSelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSelectorViewController.swift; sourceTree = ""; }; 01D8762827D459C5001140DD /* crypto-js.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "crypto-js.js"; path = "IINA+/WebFiles/node_modules/crypto-js/crypto-js.js"; sourceTree = SOURCE_ROOT; }; @@ -407,6 +413,8 @@ 016792E4212BDEE5003517A7 /* SelectVideoCollectionViewItem.swift */, 016792ED212C421F003517A7 /* SelectVideoCollectionViewItemView.swift */, 016792E5212BDEE5003517A7 /* SelectVideoCollectionViewItem.xib */, + 01B82B3828EEF9D300928F61 /* SelectVideoCollectionViewHeader.swift */, + 01B82B3628EEF74800928F61 /* SelectVideoCollectionViewHeader.xib */, ); path = SelectVideoCollectionView; sourceTree = ""; @@ -445,6 +453,7 @@ 018F4F6B2817FFF30045B67C /* BiliLive.swift */, 01683DDA2118905D0016A886 /* Bilibili.swift */, 018739ED2852135200156F3F /* QQLive.swift */, + 01C338B428E47B3F004CC0B8 /* MBGA.swift */, ); path = VideoDecoder; sourceTree = ""; @@ -660,6 +669,7 @@ 01C0807027BB5CF300E87A8C /* JSPlayer.storyboard in Resources */, 016792E7212BDEE5003517A7 /* SelectVideoCollectionViewItem.xib in Resources */, 01E40FF622C39E7100864118 /* Localizable.strings in Resources */, + 01B82B3728EEF74800928F61 /* SelectVideoCollectionViewHeader.xib in Resources */, 013850FA214EA2AA003817CE /* huya.js in Resources */, 01AEC8B220EDFD02001406E8 /* Main.storyboard in Resources */, 01892C8327C27FAB00494AFD /* douyin.html in Resources */, @@ -704,6 +714,7 @@ 016792E6212BDEE5003517A7 /* SelectVideoCollectionViewItem.swift in Sources */, 0101013921200DC5002F0F7F /* LiveUrlTableCellView.swift in Sources */, 018F4F6A2812D95C0045B67C /* CC163.swift in Sources */, + 01C338B528E47B3F004CC0B8 /* MBGA.swift in Sources */, 01479CD3210AF5F40046AAAD /* DataManager.swift in Sources */, 01398985210F27A600B7042F /* PreferencesWindowController.swift in Sources */, 01AEC8BE20EE108B001406E8 /* Processes.swift in Sources */, @@ -742,6 +753,7 @@ 015EDE27273D333900271901 /* LiveStateTransformers.swift in Sources */, 0120934427A4F632002C7FD3 /* JSPlayerWebView.swift in Sources */, 01AEC8BC20EDFFBD001406E8 /* YouGetJSON.swift in Sources */, + 01B82B3928EEF9D300928F61 /* SelectVideoCollectionViewHeader.swift in Sources */, 0132B2E52123D68D001EB7DC /* BilibiliCardImageBoxView.swift in Sources */, 01683DD9211869AE0016A886 /* BilibiliLoginViewController.swift in Sources */, 010F0F1320FE1DD100F33553 /* Preferences.swift in Sources */, @@ -944,7 +956,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 0.6.18; + MARKETING_VERSION = 0.6.19; PRODUCT_BUNDLE_IDENTIFIER = "com.xjbeta.iina-plus"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -969,7 +981,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 0.6.18; + MARKETING_VERSION = 0.6.19; PRODUCT_BUNDLE_IDENTIFIER = "com.xjbeta.iina-plus"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/IINA+.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/IINA+.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 256253ac..76befb6d 100644 --- a/IINA+.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/IINA+.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/SDWebImage/SDWebImage", "state" : { - "revision" : "484bc774e1091f622c4856e576ff957b29403676", - "version" : "5.13.3" + "revision" : "9248fe561a2a153916fb9597e3af4434784c6d32", + "version" : "5.13.4" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "8e27997de02457a0c4690ffcfb8ac9c8e1066883", - "version" : "2.2.2" + "revision" : "2a98381dfe72e24bf593c5c06d2c4fc1763c3f19", + "version" : "2.3.0" } }, { diff --git a/IINA+/Utils/VideoDecoder/BiliLive.swift b/IINA+/Utils/VideoDecoder/BiliLive.swift index 7842c70d..012869cb 100644 --- a/IINA+/Utils/VideoDecoder/BiliLive.swift +++ b/IINA+/Utils/VideoDecoder/BiliLive.swift @@ -110,6 +110,53 @@ class BiliLive: NSObject, SupportSiteProtocol { } } } + + func getRoomList(_ url: String) -> Promise<(String, [BiliLiveVideoSelector])> { + var re = [BiliLiveVideoSelector]() + + return AF.request(url).responseString().map { res -> [BiliLiveVideoSelector] in + let s = res.string.subString(from: "window.__initialState = ", to: ";\n") + guard let data = s.data(using: .utf8), + let json: JSONObject = try? JSONParser.JSONObjectWithData(data) else { return [] } + + let list: [BiliLiveRoomList] = try json.value(for: "live-non-revenue-player") + + re = list.first?.roomList.enumerated().map { + BiliLiveVideoSelector( + id: $0.element.roomId, + sid: "", + index: $0.offset, + title: $0.element.tabText, url: "") + } ?? [] + return re + }.then { + self.liveInfos($0.compactMap({ Int($0.id) })) + }.map { + guard let json: JSONObject = try? JSONParser.JSONObjectWithData($0) else { return ("", []) } + + try re.enumerated().forEach { + let info: BiliLiveBaseInfo = try json.value(for: "data.by_room_ids.\($0.element.id)") + re[$0.offset].isLiving = info.isLiving + re[$0.offset].url = info.url + re[$0.offset].sid = "\(info.shortId)" + } + return ("", re) + } + } + + func liveInfos(_ roomIds: [Int]) -> Promise { + let s = roomIds.filter { + $0 > 0 + }.map { + "room_ids=\($0)" + }.joined(separator: "&") + + guard s.count > 0 else { return .value(Data()) } + + let u = "https://api.live.bilibili.com/xlive/web-room/v1/index/getRoomBaseInfo?\(s)&req_biz=web_room_componet" + + return AF.request(u).responseData().map({ $0.data }) + } } struct BiliLiveInfo: Unmarshaling, LiveInfo { @@ -133,6 +180,19 @@ struct BiliLiveInfo: Unmarshaling, LiveInfo { } } +struct BiliLiveBaseInfo: Unmarshaling { + let roomId: Int + let shortId: Int + let isLiving: Bool + let url: String + + init(object: MarshaledObject) throws { + roomId = try object.value(for: "room_id") + shortId = try object.value(for: "short_id") + isLiving = try object.value(for: "live_status") == 1 + url = try object.value(for: "live_url") + } +} struct BiliLiveOldPlayUrl: Unmarshaling { let currentQuality: Int @@ -263,7 +323,7 @@ struct BiliLivePlayUrl: Unmarshaling { var s = Stream(url: "") s.quality = $0.qn if codec.currentQn == $0.qn { - var urls = codec.urls() + var urls = MBGA.update(codec.urls()) s.url = urls.removeFirst() s.src = urls } @@ -284,3 +344,35 @@ struct BiliLivePlayUrl: Unmarshaling { return json } } + +struct BiliLiveRoomList: Unmarshaling { + let defaultRoomId: String + let roomList: [Room] + + struct Room: Unmarshaling { + let roomId: String + let tabText: String + init(object: MarshaledObject) throws { + roomId = try object.value(for: "roomId") + tabText = try object.value(for: "tabText") + } + } + + init(object: MarshaledObject) throws { + defaultRoomId = try object.value(for: "defaultRoomId") + roomList = try object.value(for: "roomsConfig") + } +} + + +struct BiliLiveVideoSelector: VideoSelector { + let id: String + var sid: String + var coverUrl: URL? + var isLiving: Bool = false + + let site = SupportSites.biliLive + let index: Int + let title: String + var url: String +} diff --git a/IINA+/Utils/VideoDecoder/Bilibili.swift b/IINA+/Utils/VideoDecoder/Bilibili.swift index 9187f064..759d369b 100644 --- a/IINA+/Utils/VideoDecoder/Bilibili.swift +++ b/IINA+/Utils/VideoDecoder/Bilibili.swift @@ -242,7 +242,8 @@ class Bilibili: NSObject, SupportSiteProtocol { switch bUrl.urlType { case .video: json.site = .bilibili - return getVideoList(url).compactMap { list -> YouGetJSON? in + return getVideoList(url).compactMap { eps -> YouGetJSON? in + let list = eps.flatMap({ $0.1 }) var selector = list.first if let s = selector, s.isCollection { selector = list.first(where: { $0.bvid == bUrl.id }) @@ -251,7 +252,7 @@ class Bilibili: NSObject, SupportSiteProtocol { } guard let s = selector else { return nil } - json.id = s.id + json.id = Int(s.id) ?? -1 json.bvid = s.bvid json.title = s.title json.duration = Int(s.duration) @@ -370,7 +371,7 @@ class Bilibili: NSObject, SupportSiteProtocol { } } - func getVideoList(_ url: String) -> Promise<[BiliVideoSelector]> { + func getVideoList(_ url: String) -> Promise<[(String, [BiliVideoSelector])]> { var aid = -1 var bvid = "" @@ -411,7 +412,7 @@ class Bilibili: NSObject, SupportSiteProtocol { infos.enumerated().forEach { infos[$0.offset].bvid = bvid } - return infos + return [("", infos)] } } } @@ -559,11 +560,14 @@ protocol BilibiliVideoSelector: VideoSelector { } struct BiliVideoSelector: Unmarshaling, BilibiliVideoSelector { + var url: String = "" + var isLiving: Bool = false + var bvid = "" var isCollection = false // epid - let id: Int + let id: String var index: Int let part: String var duration: Int @@ -580,7 +584,8 @@ struct BiliVideoSelector: Unmarshaling, BilibiliVideoSelector { } init(object: MarshaledObject) throws { - id = try object.value(for: "cid") + let cid: Int = try object.value(for: "cid") + id = "\(cid)" if let pic: String = try? object.value(for: "arc.pic") { coverUrl = .init(string: pic) @@ -603,7 +608,7 @@ struct BiliVideoSelector: Unmarshaling, BilibiliVideoSelector { } init(ep: BangumiInfo.BangumiEp) { - id = ep.id + id = "\(ep.id)" index = -1 part = "" duration = 0 @@ -625,7 +630,7 @@ struct BilibiliVideoCollection: Unmarshaling { let mid: Int let epCount: Int let isPaySeason: Bool - let episodes: [BiliVideoSelector] + let episodes: [(String, [BiliVideoSelector])] init(object: MarshaledObject) throws { id = try object.value(for: "id") @@ -636,7 +641,9 @@ struct BilibiliVideoCollection: Unmarshaling { isPaySeason = try object.value(for: "is_pay_season") let s: [Section] = try object.value(for: "sections") - episodes = s.first?.episodes ?? [] + episodes = s.map { + ($0.title, $0.episodes) + } } struct Section: Unmarshaling { @@ -853,10 +860,16 @@ struct BilibiliPlayInfo: Unmarshaling { yougetJson.duration = duration videos.enumerated().forEach { - var stream = Stream(url: $0.element.url) + var urls = $0.element.backupUrl + urls.append($0.element.url) + urls = MBGA.update(urls) + + var stream = Stream(url: "") // stream.quality = $0.element.bandwidth stream.quality = 999 - $0.element.index - stream.src = $0.element.backupUrl + + stream.url = urls.removeFirst() + stream.src = urls yougetJson.streams[$0.element.description] = stream } @@ -905,8 +918,12 @@ struct BilibiliSimplePlayInfo: Unmarshaling { var stream = yougetJson.streams[$0.value] ?? Stream(url: "") if $0.key == quality, let durl = durl.first { - stream.url = durl.url - stream.src = durl.backupUrls + var urls = durl.backupUrls + urls.append(durl.url) + urls = MBGA.update(urls) + + stream.url = urls.removeFirst() + stream.src = urls } stream.quality = $0.key yougetJson.streams[$0.value] = stream diff --git a/IINA+/Utils/VideoDecoder/CC163.swift b/IINA+/Utils/VideoDecoder/CC163.swift index 998041ee..e0ff0298 100644 --- a/IINA+/Utils/VideoDecoder/CC163.swift +++ b/IINA+/Utils/VideoDecoder/CC163.swift @@ -74,18 +74,24 @@ class CC163: NSObject, SupportSiteProtocol { func getCC163ZtRoomList(_ text: String) throws -> [CC163ZTInfo] { try SwiftSoup.parse(text) .getElementsByClass("channel_list").first()? - .children().map { + .children().compactMap { item -> CC163ZTInfo? in + func findAttr(_ key: String) -> String { + (try? item.getElementsByAttribute(key).first()?.attr(key)) ?? "" + } - CC163ZTInfo( - name: try $0.children().first()?.children().first()?.text() ?? "", - ccid: try $0.attr("ccid"), - channel: try ($0.attr("channel").starts(with: "https:") ? $0.attr("channel") : "https:" + $0.attr("channel")), - cid: try $0.attr("cid"), - index: try $0.attr("index"), - roomid: try $0.attr("roomid"), - isLiving: $0.children().first()?.children().hasClass("icon-live") ?? false) - }.filter { - $0.ccid != "" && $0.roomid != "" + let info = CC163ZTInfo( + name: try item.text(), + ccid: findAttr("ccid"), + channel: (findAttr("channel").starts(with: "https:") || findAttr("channel") == "") ? findAttr("channel") : "https:" + findAttr("channel"), + cid: findAttr("cid"), + index: findAttr("index"), + roomid: findAttr("roomid"), + isLiving: (try? item.getElementsByClass("icon-live").first()) != nil) + if (info.ccid != "" || info.roomid != ""), info.channel != "" { + return info + } else { + return nil + } } ?? [] } @@ -186,7 +192,7 @@ struct CC163VideoSelector: VideoSelector { let ccid: String let isLiving: Bool let url: String - let id: Int = -1 + let id: String let coverUrl: URL? = nil } @@ -221,14 +227,18 @@ struct CC163ChannelInfo: Unmarshaling, LiveInfo { } } +protocol CC163Video { + var vbr: Int { get } + var urls: [String] { get set } +} struct CC163NewVideos: Unmarshaling { let title: String - let videos: [String: VideoItem] + let videos: [String: CC163Video] - struct VideoItem: Unmarshaling { + struct VideoItem: CC163Video, Unmarshaling { let vbr: Int - let urls: [String] + var urls: [String] init(object: MarshaledObject) throws { vbr = try object.value(for: "vbr") @@ -238,8 +248,39 @@ struct CC163NewVideos: Unmarshaling { } } + struct StramItem: CC163Video, Unmarshaling { + let vbr: Int + var urls: [String] + + let streamname: String + + init(object: MarshaledObject) throws { + vbr = try object.value(for: "vbr") + streamname = try object.value(for: "streamname") + let cdns: [String: String] = try object.value(for: "CDN_FMT") + urls = [ + + ] + + if let v = cdns["ali"] { + urls.append("https://alipullhdlptscopy.cc.netease.com/pushstation/\(streamname).flv?\(v)") + } + + if let v = cdns["ks"] { + urls.append("https://kspullhdlptscopy.cc.netease.com/pushstation/\(streamname).flv?\(v)") + } + } + } + init(object: MarshaledObject) throws { - videos = try object.value(for: "quickplay.resolution") + videos = try { + if let re: [String: VideoItem] = try? object.value(for: "quickplay.resolution") { + return re + } else { + let re: [String: StramItem] = try object.value(for: "stream_list") + return re + } + }() title = try object.value(for: "title") } diff --git a/IINA+/Utils/VideoDecoder/DouYin.swift b/IINA+/Utils/VideoDecoder/DouYin.swift index 97ab9e71..bf9cb461 100644 --- a/IINA+/Utils/VideoDecoder/DouYin.swift +++ b/IINA+/Utils/VideoDecoder/DouYin.swift @@ -64,7 +64,7 @@ class DouYin: NSObject, SupportSiteProtocol { let headers = HTTPHeaders([ "User-Agent": douyinUA, - "referer": "https://live.douyin.com", + "referer": url, "Cookie": cookieString ]) diff --git a/IINA+/Utils/VideoDecoder/Douyu.swift b/IINA+/Utils/VideoDecoder/Douyu.swift index 7b4ac5cf..01c232dd 100644 --- a/IINA+/Utils/VideoDecoder/Douyu.swift +++ b/IINA+/Utils/VideoDecoder/Douyu.swift @@ -125,7 +125,20 @@ class Douyu: NSObject, SupportSiteProtocol { } let json: JSONObject = try JSONParser.JSONObjectWithData(data) - return try json.value(for: "children") + return (try json.value(for: "children")).filter { + $0.roomId != "" + } + } + } + + func getDouyuEventRoomOnlineStatus(_ pageId: String) -> Promise<[String: Bool]> { + + struct RoomOnlineStatus: Decodable { + let data: [String: Bool] + } + + return AF.request("https://www.douyu.com/japi/carnival/c/roomActivity/getRoomOnlineStatus?pageId=\(pageId)").responseDecodable(RoomOnlineStatus.self).map { + $0.data } } @@ -270,15 +283,26 @@ struct DouyuVideoSelector: VideoSelector { let site = SupportSites.douyu let index: Int let title: String - let id: Int + let id: String + let url: String + var isLiving: Bool let coverUrl: URL? } struct DouyuEventRoom: Unmarshaling { - let onlineRoomId: String + let roomId: String let text: String init(object: MarshaledObject) throws { - onlineRoomId = try object.value(for: "props.onlineRoomId") + if let rid: String = try? object.value(for: "props.onlineRoomId") { + roomId = rid + } else if let rid: String = try? object.value(for: "props.liveRoomId") { + roomId = rid + } else { + roomId = "" + text = "" + return + } + text = try object.value(for: "props.text") } } diff --git a/IINA+/Utils/VideoDecoder/Huya.swift b/IINA+/Utils/VideoDecoder/Huya.swift index 97981632..63f5e939 100644 --- a/IINA+/Utils/VideoDecoder/Huya.swift +++ b/IINA+/Utils/VideoDecoder/Huya.swift @@ -11,6 +11,7 @@ import PromiseKit import Alamofire import PMKAlamofire import Marshal +import SwiftSoup class Huya: NSObject, SupportSiteProtocol { @@ -40,6 +41,34 @@ class Huya: NSObject, SupportSiteProtocol { // MARK: - Huya + struct HuyaRoomList { + var current: String + var list = [HuyaVideoSelector]() + } + + + // href, name + func getHuyaRoomList(_ url: String) -> Promise { + AF.request(url).responseString().map { + var re = HuyaRoomList(current: "") + try SwiftSoup.parse($0.string).getElementsByClass("match-nav").first()?.children().enumerated().forEach { + + if try $0.element.attr("class") == "on" { + re.current = try $0.element.attr("href") + } + + try re.list.append(.init( + id: $0.element.attr("href"), + index: $0.offset, + title: $0.element.text(), + url: "https://www.huya.com/\($0.element.attr("href"))", + isLiving: $0.element.getChildNodes().contains(where: { try $0.attr("class") == "live" }) + )) + } + return re + } + } + func getHuyaInfo(_ url: String) -> Promise<(HuyaInfo, [(String, Stream)])> { // https://github.com/zhangn1985/ykdl/blob/master/ykdl/extractors/huya/live.py AF.request(url).responseString().map { @@ -428,3 +457,14 @@ fileprivate func huyaUrlFormatter2(_ u: String) -> String? { return uc.url?.absoluteString } + +struct HuyaVideoSelector: VideoSelector { + var id: String + var coverUrl: URL? + + let site = SupportSites.huya + let index: Int + let title: String + let url: String + let isLiving: Bool +} diff --git a/IINA+/Utils/VideoDecoder/MBGA.swift b/IINA+/Utils/VideoDecoder/MBGA.swift new file mode 100644 index 00000000..fbc9160b --- /dev/null +++ b/IINA+/Utils/VideoDecoder/MBGA.swift @@ -0,0 +1,61 @@ +// +// MBGA.swift +// IINA+ +// +// Created by xjbeta on 2022/9/28. +// Copyright © 2022 xjbeta. All rights reserved. +// + +import Foundation + +// Make Bilibili Great Again! +// https://greasyfork.org/zh-CN/scripts/415714-make-bilibili-grate-again + +class MBGA: NSObject { + enum BilibiliCDN: Int { + case mirror, cache, mcdn, pcdn + func name() -> String { + switch self { + case .mirror: return "Mirror" + case .cache: return "Cache" + case .mcdn: return "MCDN" + case .pcdn: return "PCDN" + } + } + } + + static func update(_ urls: [String]) -> [String] { + urls.compactMap { u -> String? in + guard var uc = URLComponents(string: u), + let host = uc.host else { return u } + + if host.hasSuffix(".mcdn.bilivideo.cn") { +// document.head.innerHTML.match(/up[\w-]+\.bilivideo\.com/) +// uc.host = "upos-sz-mirrorcoso1.bilivideo.com" +// uc.port = 443 + } else if host.hasSuffix(".szbdyd.com"), + let host = uc.queryItems?.first(where: { $0.name == "xy_usource" })?.value { + uc.host = host + uc.port = 443 + } + return uc.string + }.sorted { u1, u2 in + cdnLevel(for: u1).rawValue < cdnLevel(for: u2).rawValue + } + } + + static func cdnLevel(for url: String) -> BilibiliCDN { + guard var uc = URLComponents(string: url), + let host = uc.host else { return .mcdn } + + if host.hasSuffix(".mcdn.bilivideo.cn") { + return .mcdn + } else if host.hasSuffix(".szbdyd.com") { + return .pcdn + } else if host.hasSuffix("bilivideo.com") && host.hasPrefix("up") { + return .mirror + } else { + return .cache + } + } +} diff --git a/IINA+/Utils/VideoDecoder/VideoGetStructs.swift b/IINA+/Utils/VideoDecoder/VideoGetStructs.swift index b914171d..16dde477 100644 --- a/IINA+/Utils/VideoDecoder/VideoGetStructs.swift +++ b/IINA+/Utils/VideoDecoder/VideoGetStructs.swift @@ -23,7 +23,9 @@ protocol VideoSelector { var site: SupportSites { get } var index: Int { get } var title: String { get } - var id: Int { get } + var id: String { get } + var url: String { get } + var isLiving: Bool { get } var coverUrl: URL? { get } } diff --git a/IINA+/Views/Base.lproj/Main.storyboard b/IINA+/Views/Base.lproj/Main.storyboard index 0c8b1bd4..76f63a92 100644 --- a/IINA+/Views/Base.lproj/Main.storyboard +++ b/IINA+/Views/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -323,19 +323,19 @@ - + - + - + - + @@ -728,7 +728,7 @@ - + @@ -881,6 +881,7 @@ + @@ -1168,6 +1169,22 @@ + + + + + + + + + + + + + + + + diff --git a/IINA+/Views/MainWindow/MainViewController.swift b/IINA+/Views/MainWindow/MainViewController.swift index 58fb01e6..6b58b922 100644 --- a/IINA+/Views/MainWindow/MainViewController.swift +++ b/IINA+/Views/MainWindow/MainViewController.swift @@ -57,7 +57,7 @@ class MainViewController: NSViewController { @IBAction func deleteBookmark(_ sender: Any) { guard let index = bookmarkTableView.selectedIndexs().first, let objs = bookmarkArrayController.arrangedObjects as? [Bookmark], - index > 0, + index >= 0, index < objs.count, let w = view.window else { return } let obj = objs[index] @@ -87,13 +87,30 @@ class MainViewController: NSViewController { } @IBAction func copyUrl(_ sender: NSMenuItem) { - let url = bookmarks[bookmarkTableView.clickedRow].url + var url: String? + switch sender.menu { + case bookmarkTableView.menu: + url = bookmarks[bookmarkTableView.clickedRow].url + case bilibiliTableView.menu: + url = "https://www.bilibili.com/video/" + bilibiliCards[bilibiliTableView.clickedRow].bvid + default: break + } + guard let url = url else { return } NSPasteboard.general.clearContents() NSPasteboard.general.setString(url, forType: .string) } @IBAction func decode(_ sender: NSMenuItem) { - let url = bookmarks[bookmarkTableView.clickedRow].url + var url: String? + switch sender.menu { + case bookmarkTableView.menu: + url = bookmarks[bookmarkTableView.clickedRow].url + case bilibiliTableView.menu: + url = "https://www.bilibili.com/video/" + bilibiliCards[bilibiliTableView.clickedRow].bvid + default: break + } + guard let url = url else { return } + searchField.stringValue = url searchField.becomeFirstResponder() startSearch(self) @@ -372,7 +389,7 @@ class MainViewController: NSViewController { NotificationCenter.default.post(name: .progressStatusChanged, object: nil, userInfo: ["inProgress": inProgress]) } - func showSelectVideo(_ videoId: String, infos: [VideoSelector], currentItem: Int = 0) { + func showSelectVideo(_ videoId: String, infos: [(String, [VideoSelector])], currentItem: Int = 0) { guard let selectVideoViewController = self.children.compactMap({ $0 as? SelectVideoViewController }).first else { return } @@ -491,8 +508,9 @@ class MainViewController: NSViewController { switch bUrl.urlType { case .video: re = bilibili.getVideoList(u).done { infos in - if infos.count > 1 { - let cItem = infos.first!.isCollection ? infos.firstIndex(where: { $0.bvid == bUrl.id }) : bUrl.p - 1 + let list = infos.flatMap({ $0.1 }) + if list.count > 1 { + let cItem = list.first!.isCollection ? list.firstIndex(where: { $0.bvid == bUrl.id }) : bUrl.p - 1 self.showSelectVideo(bUrl.id, infos: infos, currentItem: cItem ?? 0) resolver.fulfill(()) } else { @@ -506,14 +524,13 @@ class MainViewController: NSViewController { decodeUrl() } else { var cItem = 0 - if bUrl.id.starts(with: "ep"), - let epId = Int(bUrl.id.dropFirst(2)) { + if bUrl.id.starts(with: "ep") { cItem = epVS.firstIndex { - $0.id == epId + $0.id == bUrl.id.dropFirst(2) } ?? 0 } - self.showSelectVideo("", infos: epVS, currentItem: cItem) + self.showSelectVideo("", infos: [("", epVS)], currentItem: cItem) resolver.fulfill(()) } } @@ -527,22 +544,31 @@ class MainViewController: NSViewController { url.pathComponents.count > 2, url.pathComponents[1] == "topic" { let douyu = videoGet.douyu - douyu.getDouyuHtml(str).done { - guard $0.roomIds.count > 0 else { + douyu.getDouyuHtml(str).done { htmls in + guard htmls.roomIds.count > 0 else { decodeUrl() return } - let cid = $0.roomId - douyu.getDouyuEventRoomNames($0.pageId).done { - let infos = $0.enumerated().map { + let cid = htmls.roomId + var re = [DouyuVideoSelector]() + + douyu.getDouyuEventRoomNames(htmls.pageId).get { + re = $0.enumerated().map { DouyuVideoSelector( index: $0.offset, title: $0.element.text, - id: Int($0.element.onlineRoomId) ?? 0, + id: $0.element.roomId, + url: "https://www.douyu.com/\($0.element.roomId)", + isLiving: false, coverUrl: nil) } - - self.showSelectVideo("", infos: infos, currentItem: $0.map({ $0.onlineRoomId }).firstIndex(of: cid) ?? 0) + }.then { _ in + douyu.getDouyuEventRoomOnlineStatus(htmls.pageId) + }.done { status in + re.enumerated().forEach { + re[$0.offset].isLiving = status[$0.element.id] ?? false + } + self.showSelectVideo("", infos: [("", re)], currentItem: re.map({ $0.id }).firstIndex(of: cid) ?? 0) resolver.fulfill(()) }.catch { switch $0 { @@ -555,6 +581,34 @@ class MainViewController: NSViewController { }.catch { resolver.reject($0) } + } else if url.host == "www.huya.com" { + videoGet.huya.getHuyaRoomList(url.absoluteString).done { rl in + if rl.list.count == 0 { + decodeUrl() + } else { + self.showSelectVideo("", infos: [("", rl.list)], currentItem: rl.list.firstIndex(where: { $0.id == rl.current }) ?? 0) + resolver.fulfill(()) + } + }.catch { + resolver.reject($0) + } + } else if url.host == "live.bilibili.com" { + videoGet.biliLive.getRoomList(url.absoluteString).done { + if $0.1.count == 0 { + decodeUrl() + } else { + var c = 0 + if url.pathComponents.count > 1 { + let id = "\(url.pathComponents[1])" + c = $0.1.firstIndex(where: { $0.id == id || $0.sid == id }) ?? 0 + } + + self.showSelectVideo("", infos: [("", $0.1)], currentItem: c) + resolver.fulfill(()) + } + }.catch { + resolver.reject($0) + } } else if url.host == "cc.163.com" { videoGet.cc163.getCC163State(url.absoluteString).done { if let i = $0.info as? CC163Info { @@ -572,9 +626,10 @@ class MainViewController: NSViewController { title: $0.element.name, ccid: $0.element.ccid, isLiving: $0.element.isLiving, - url: $0.element.channel) + url: $0.element.channel, + id: $0.element.ccid) } - self.showSelectVideo("", infos: infos) + self.showSelectVideo("", infos: [("", infos)]) resolver.fulfill(()) } }.catch { diff --git a/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoCollectionViewHeader.swift b/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoCollectionViewHeader.swift new file mode 100644 index 00000000..44cae1a7 --- /dev/null +++ b/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoCollectionViewHeader.swift @@ -0,0 +1,21 @@ +// +// SelectVideoCollectionViewHeader.swift +// IINA+ +// +// Created by xjbeta on 2022/10/6. +// Copyright © 2022 xjbeta. All rights reserved. +// + +import Cocoa + +class SelectVideoCollectionViewHeader: NSView { + + @IBOutlet weak var titleTextField: NSTextField! + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + // Drawing code here. + } + +} diff --git a/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoCollectionViewHeader.xib b/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoCollectionViewHeader.xib new file mode 100644 index 00000000..5f616083 --- /dev/null +++ b/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoCollectionViewHeader.xib @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoViewController.swift b/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoViewController.swift index 4b26dce1..a378b763 100644 --- a/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoViewController.swift +++ b/IINA+/Views/MainWindow/SelectVideoCollectionView/SelectVideoViewController.swift @@ -13,9 +13,15 @@ class SelectVideoViewController: NSViewController { @IBOutlet weak var collectionView: NSCollectionView! var currentItem = 0 - var videoInfos: [VideoSelector] = [] { + var videoInfos: [(String, [VideoSelector])] = [] { didSet { - if let max = videoInfos.map({ $0.title.count }).max() { + let length = videoInfos.flatMap { + $0.1 + }.map { + $0.title.count + }.max() + + if let max = length { var size: NSSize? = nil switch max { case _ where max > 40: @@ -42,40 +48,54 @@ class SelectVideoViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() + ( + collectionView.collectionViewLayout as? NSCollectionViewFlowLayout + )?.sectionHeadersPinToVisibleBounds = true + } + + func videoInfos(at section: Int) -> [VideoSelector] { + videoInfos[section - (currentItem > 0 ? 1 : 0)].1 } func videoInfo(at indexPath: IndexPath) -> VideoSelector? { switch indexPath.section { case 0 where currentItem > 0: - return videoInfos[currentItem] - case 0: - return videoInfos[indexPath.item] - case 1 where currentItem > 0: - return videoInfos[indexPath.item] + return videoInfos.flatMap { + $0.1 + }[currentItem] default: - return nil + return videoInfos(at: indexPath.section)[indexPath.item] } } } +extension SelectVideoViewController: NSCollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> NSSize { + if currentItem > 0 && section == 0 { + return .zero + } else if videoInfos.count <= 1 { + return .zero + } else { + return .init(width: 1000, height: 30) + } + } +} + extension SelectVideoViewController: NSCollectionViewDataSource, NSCollectionViewDelegate { func numberOfSections(in collectionView: NSCollectionView) -> Int { - currentItem > 0 ? 2 : 1 + videoInfos.count + (currentItem > 0 ? 1 : 0) } func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { - let c = videoInfos.count - - switch section { - case 0: - return currentItem > 0 ? 1 : c - case 1: - return c - default: - return 0 - } + (currentItem > 0 && section == 0) ? 1 : videoInfos(at: section).count + } + + func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, at indexPath: IndexPath) -> NSView { + let view = collectionView.makeSupplementaryView(ofKind: kind, withIdentifier: .init(rawValue: "SelectVideoCollectionViewHeader"), for: indexPath) + (view as? SelectVideoCollectionViewHeader)?.titleTextField.stringValue = videoInfo(at: indexPath)?.title ?? "" + return view } func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { @@ -95,14 +115,8 @@ extension SelectVideoViewController: NSCollectionViewDataSource, NSCollectionVie if let longTitle = (info as? BiliVideoSelector)?.longTitle { s += " \(longTitle)" } - case .douyu: - s = info.title - case .cc163: - let i = info as! CC163VideoSelector - s = i.title - if i.isLiving { - s += " - 直播中" - } + case .douyu, .huya, .biliLive, .cc163: + s = (info.isLiving ? "🔥" : "") + info.title default: break } @@ -141,8 +155,8 @@ extension SelectVideoViewController: NSCollectionViewDataSource, NSCollectionVie } else { u = "https://www.bilibili.com/video/\(videoId)?p=\(info.index)" } - case .douyu: - u = "https://www.douyu.com/\(info.id)" + case .douyu, .huya, .biliLive: + u = info.url case .bangumi: u = "https://www.bilibili.com/bangumi/play/ep\(info.id)" case .cc163: diff --git a/IINA+/Views/MainWindow/TableViewCustomViews/BilibiliCardImageBoxView.swift b/IINA+/Views/MainWindow/TableViewCustomViews/BilibiliCardImageBoxView.swift index 2a970448..8a3284e5 100644 --- a/IINA+/Views/MainWindow/TableViewCustomViews/BilibiliCardImageBoxView.swift +++ b/IINA+/Views/MainWindow/TableViewCustomViews/BilibiliCardImageBoxView.swift @@ -11,7 +11,13 @@ import Cocoa class BilibiliCardImageBoxView: NSView { var pic: NSImage? = nil var pImages: [NSImage] = [] - var aid: Int = 0 + var pAid: Int = -1 + var aid: Int = 0 { + didSet { + pAid = -1 + pImages = [] + } + } var displayedIndex = -1 var state: PreviewStatus = .init🐴 @@ -79,9 +85,11 @@ class BilibiliCardImageBoxView: NSView { switch status { case .init🐴: state = .init🐴 - if pImages.count == 0 { - Processes.shared.videoDecoder.bilibili.getPvideo(aid).done(on: .main) { pvideo in + if pImages.count == 0 || pAid != aid { + let id = aid + Processes.shared.videoDecoder.bilibili.getPvideo(id).done(on: .main) { pvideo in self.pImages = pvideo.pImages + self.pAid = id self.updatePreview(.start, per: self.previewPercent) }.catch { error in Log("Error when get pImages: \(error)") diff --git a/IINA+/Views/Preferences/Base.lproj/Preferences.storyboard b/IINA+/Views/Preferences/Base.lproj/Preferences.storyboard index 06cda600..26db2ca7 100644 --- a/IINA+/Views/Preferences/Base.lproj/Preferences.storyboard +++ b/IINA+/Views/Preferences/Base.lproj/Preferences.storyboard @@ -1,8 +1,8 @@ - + - + @@ -45,6 +45,9 @@ + + + @@ -149,8 +152,8 @@ - - + + @@ -397,18 +400,18 @@ - + - + - + @@ -416,14 +419,14 @@ - + - + @@ -441,7 +444,7 @@