Add Invidious comments support

This commit is contained in:
Arkadiusz Fal 2022-07-02 00:14:04 +02:00
parent 4fcf57d755
commit 7c4ee9bf35
7 changed files with 43 additions and 70 deletions

View File

@ -157,6 +157,15 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
configureTransformer(pathPattern("videos/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Video in configureTransformer(pathPattern("videos/*"), requestMethods: [.get]) { (content: Entity<JSON>) -> Video in
self.extractVideo(from: content.json) self.extractVideo(from: content.json)
} }
configureTransformer(pathPattern("comments/*")) { (content: Entity<JSON>) -> CommentsPage in
let details = content.json.dictionaryValue
let comments = details["comments"]?.arrayValue.compactMap { self.extractComment(from: $0) } ?? []
let nextPage = details["continuation"]?.string
let disabled = !details["error"].isNil
return CommentsPage(comments: comments, nextPage: nextPage, disabled: disabled)
}
} }
private func pathPattern(_ path: String) -> String { private func pathPattern(_ path: String) -> String {
@ -337,7 +346,12 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
.withParam("q", query.lowercased()) .withParam("q", query.lowercased())
} }
func comments(_: Video.ID, page _: String?) -> Resource? { nil } func comments(_ id: Video.ID, page: String?) -> Resource? {
let resource = resource(baseURL: account.url, path: basePathAppending("comments/\(id)"))
guard let page = page else { return resource }
return resource.withParam("continuation", page)
}
private func searchQuery(_ query: String) -> String { private func searchQuery(_ query: String) -> String {
var searchQuery = query var searchQuery = query
@ -533,4 +547,23 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
videos: content["videos"].arrayValue.map { extractVideo(from: $0) } videos: content["videos"].arrayValue.map { extractVideo(from: $0) }
) )
} }
private func extractComment(from content: JSON) -> Comment? {
let details = content.dictionaryValue
let author = details["author"]?.string ?? ""
let channelId = details["authorId"]?.string ?? UUID().uuidString
let authorAvatarURL = details["authorThumbnails"]?.arrayValue.last?.dictionaryValue["url"]?.string ?? ""
return Comment(
id: UUID().uuidString,
author: author,
authorAvatarURL: authorAvatarURL,
time: details["publishedText"]?.string ?? "",
pinned: false,
hearted: false,
likeCount: details["likeCount"]?.int ?? 0,
text: details["content"]?.string ?? "",
repliesPage: details["replies"]?.dictionaryValue["continuation"]?.string,
channel: Channel(id: channelId, name: author)
)
}
} }

View File

@ -51,10 +51,6 @@ enum VideosApp: String, CaseIterable {
self == .piped self == .piped
} }
var supportsComments: Bool {
self == .piped
}
var searchUsesIndexedPages: Bool { var searchUsesIndexedPages: Bool {
self == .invidious self == .invidious
} }

View File

@ -17,38 +17,16 @@ final class CommentsModel: ObservableObject {
var player: PlayerModel! var player: PlayerModel!
static var instance: Instance? { var instance: Instance? {
InstancesModel.find(Defaults[.commentsInstanceID]) player.accounts.current?.instance
} }
var api: VideosAPI? {
Self.instance.isNil ? nil : PipedAPI(account: Self.instance!.anonymousAccount)
}
static var enabled: Bool {
!instance.isNil
}
#if !os(tvOS)
static var placement: CommentsPlacement {
Defaults[.commentsPlacement]
}
#endif
var nextPageAvailable: Bool { var nextPageAvailable: Bool {
!(nextPage?.isEmpty ?? true) !(nextPage?.isEmpty ?? true)
} }
func load(page: String? = nil) { func load(page: String? = nil) {
guard Self.enabled, !loaded else { guard let video = player.currentVideo else { return }
return
}
guard !Self.instance.isNil,
let video = player.currentVideo
else {
return
}
if !firstPage && !nextPageAvailable { if !firstPage && !nextPageAvailable {
return return
@ -56,7 +34,7 @@ final class CommentsModel: ObservableObject {
firstPage = page.isNil || page!.isEmpty firstPage = page.isNil || page!.isEmpty
api?.comments(video.videoID, page: page)? player.accounts.api.comments(video.videoID, page: page)?
.load() .load()
.onSuccess { [weak self] response in .onSuccess { [weak self] response in
if let page: CommentsPage = response.typedContent() { if let page: CommentsPage = response.typedContent() {
@ -65,6 +43,9 @@ final class CommentsModel: ObservableObject {
self?.disabled = page.disabled self?.disabled = page.disabled
} }
} }
.onFailure { [weak self] requestError in
self?.disabled = !requestError.json.dictionaryValue["error"].isNil
}
.onCompletion { [weak self] _ in .onCompletion { [weak self] _ in
self?.loaded = true self?.loaded = true
} }
@ -94,7 +75,7 @@ final class CommentsModel: ObservableObject {
repliesPageID = page repliesPageID = page
repliesLoaded = false repliesLoaded = false
api?.comments(player.currentVideo!.videoID, page: page)? player.accounts.api.comments(player.currentVideo!.videoID, page: page)?
.load() .load()
.onSuccess { [weak self] response in .onSuccess { [weak self] response in
if let page: CommentsPage = response.typedContent() { if let page: CommentsPage = response.typedContent() {

View File

@ -53,7 +53,6 @@ extension Defaults.Keys {
static let playerInstanceID = Key<Instance.ID?>("playerInstance") static let playerInstanceID = Key<Instance.ID?>("playerInstance")
static let showKeywords = Key<Bool>("showKeywords", default: false) static let showKeywords = Key<Bool>("showKeywords", default: false)
static let showHistoryInPlayer = Key<Bool>("showHistoryInPlayer", default: false) static let showHistoryInPlayer = Key<Bool>("showHistoryInPlayer", default: false)
static let commentsInstanceID = Key<Instance.ID?>("commentsInstance", default: nil)
#if !os(tvOS) #if !os(tvOS)
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate) static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
#endif #endif

View File

@ -255,23 +255,8 @@ struct VideoDetails: View {
} }
} }
} }
if !video.isNil, CommentsModel.placement == .info {
Divider()
#if os(macOS)
.padding(.bottom, 20)
#else
.padding(.vertical, 10)
#endif
}
} }
.padding(.horizontal) .padding(.horizontal)
LazyVStack {
if !video.isNil, CommentsModel.placement == .info {
CommentsView()
}
}
} }
} }

View File

@ -5,7 +5,6 @@ struct PlayerSettings: View {
@Default(.instances) private var instances @Default(.instances) private var instances
@Default(.playerInstanceID) private var playerInstanceID @Default(.playerInstanceID) private var playerInstanceID
@Default(.quality) private var quality @Default(.quality) private var quality
@Default(.commentsInstanceID) private var commentsInstanceID
@Default(.playerSidebar) private var playerSidebar @Default(.playerSidebar) private var playerSidebar
@Default(.showHistoryInPlayer) private var showHistory @Default(.showHistoryInPlayer) private var showHistory
@ -64,10 +63,6 @@ struct PlayerSettings: View {
closeLastItemOnPlaybackEndToggle closeLastItemOnPlaybackEndToggle
} }
Section(header: SettingsHeader(text: "Comments")) {
commentsInstancePicker
}
Section(header: SettingsHeader(text: "Interface")) { Section(header: SettingsHeader(text: "Interface")) {
#if os(iOS) #if os(iOS)
if idiom == .pad { if idiom == .pad {
@ -135,22 +130,6 @@ struct PlayerSettings: View {
#endif #endif
} }
private var commentsInstancePicker: some View {
Picker("Source", selection: $commentsInstanceID) {
Text("Disabled").tag(Optional(""))
ForEach(InstancesModel.all.filter { $0.app.supportsComments }) { instance in
Text(instance.description).tag(Optional(instance.id))
}
}
.labelsHidden()
#if os(iOS)
.pickerStyle(.automatic)
#elseif os(tvOS)
.pickerStyle(.inline)
#endif
}
private var sidebarPicker: some View { private var sidebarPicker: some View {
Picker("Sidebar", selection: $playerSidebar) { Picker("Sidebar", selection: $playerSidebar) {
#if os(macOS) #if os(macOS)

View File

@ -190,7 +190,7 @@ struct SettingsView: View {
case .browsing: case .browsing:
return 390 return 390
case .player: case .player:
return 500 return 390
case .history: case .history:
return 480 return 480
case .sponsorBlock: case .sponsorBlock: