diff --git a/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Anime.swift b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Anime.swift new file mode 100644 index 00000000..85a6a15c --- /dev/null +++ b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Anime.swift @@ -0,0 +1,98 @@ +// +// This file is part of the NineAnimator project. +// +// Copyright © 2018-2020 Marcus Zhou. All rights reserved. +// +// NineAnimator is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// NineAnimator is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with NineAnimator. If not, see . +// + +import Foundation +import NineAnimatorCommon +import SwiftSoup + +extension NASourceAnimeFlv { + func anime(from link: AnimeLink) -> NineAnimatorPromise { + self.requestManager.request( + url: link.link, handling: .browsing) + .responseString + .then { + responseContent -> Anime in + let bowl = try SwiftSoup.parse(responseContent) + let animeTitle = try bowl.select("h1.Title").text() + let animeArtworkURL = try URL( + string: bowl.select("figure.Image > img").attr("src"), + relativeTo: self.endpointURL + ) ?? link.image + let reconstructedAnimeLink = AnimeLink( + title: animeTitle, + link: link.link, + image: animeArtworkURL, + source: self + ) + // List of episode + let episodeList = try bowl.select("li.Episode").compactMap { + episodeElement -> (identifier: String, episodeNumber: String) in + let episodeIdentifier = "\(self.endpointURL)\(try episodeElement.select("a").attr("href"))" + let episodeNumber = try episodeElement + .select("a") + .text() + .replacingOccurrences(of: animeTitle, with: "") + return (episodeIdentifier, episodeNumber) + } .reversed() + + if episodeList.isEmpty { + throw NineAnimatorError.responseError("No episodes found for this anime") + } + + // Collection of episodes + var episodeCollection = Anime.EpisodesCollection() + + // We incorrectly assume each server contains every episode + for (serverIdentifier, _) in NASourceAnimeFlv.knownServers { + var currentCollection = [EpisodeLink]() + + for (episodeIdentifier, episodeName) in episodeList { + let currentEpisodeLink = EpisodeLink( + identifier: episodeIdentifier, + name: episodeName, + server: serverIdentifier, + parent: reconstructedAnimeLink + ) + currentCollection.append(currentEpisodeLink) + } + + episodeCollection[serverIdentifier] = currentCollection.reversed() + } + + // Information + let animeSynopsis = try bowl.select("header > p:nth-child(3)").text().replacingOccurrences(of: "Sinopsis: ", with: "") + + // Attributes + var additionalAnimeAttributes = [Anime.AttributeKey: Any]() + + let rating = try bowl.select("#votes_prmd").text() + additionalAnimeAttributes[.rating] = Float(rating) ?? 0 + additionalAnimeAttributes[.ratingScale] = Float(5.0) + + return Anime( + reconstructedAnimeLink, + alias: "", + additionalAttributes: additionalAnimeAttributes, + description: animeSynopsis, + on: NASourceAnimeFlv.knownServers, + episodes: episodeCollection + ) + } + } +} diff --git a/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Episode.swift b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Episode.swift new file mode 100644 index 00000000..db16292d --- /dev/null +++ b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Episode.swift @@ -0,0 +1,48 @@ +// +// This file is part of the NineAnimator project. +// +// Copyright © 2018-2020 Marcus Zhou. All rights reserved. +// +// NineAnimator is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// NineAnimator is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with NineAnimator. If not, see . +// + +import Foundation +import NineAnimatorCommon +import SwiftSoup + +extension NASourceAnimeFlv { + static let knownServers = [ + "yuserver": "YUserver" + ] + + static let urlRegex = try! NSRegularExpression(pattern: "https:\\\\/\\\\/www.+?(?=\")") + + func episode(from link: EpisodeLink, with anime: Anime) -> NineAnimatorPromise { + self.requestManager.request(url: link.identifier, handling: .browsing) + .responseString + .then { + responseContent in + let bowl = try SwiftSoup.parse(responseContent) + let scriptText = try bowl.select("#AnimeFlv > script:nth-child(16)").outerHtml() + let iframeURLString = try (NASourceAnimeFlv.urlRegex.firstMatch(in: scriptText)) + .tryUnwrap()[0].replacingOccurrences(of: "\\", with: "") + let iframeURL = try URL(string: iframeURLString).tryUnwrap() + return Episode( + link, + target: iframeURL, + parent: anime + ) + } + } +} diff --git a/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Featured.swift b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Featured.swift new file mode 100644 index 00000000..158870c2 --- /dev/null +++ b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Featured.swift @@ -0,0 +1,91 @@ +// +// This file is part of the NineAnimator project. +// +// Copyright © 2018-2020 Marcus Zhou. All rights reserved. +// +// NineAnimator is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// NineAnimator is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with NineAnimator. If not, see . +// + +import Foundation +import NineAnimatorCommon +import SwiftSoup + +extension NASourceAnimeFlv { + func featured() -> NineAnimatorPromise { + self.requestManager.request( + url: endpointURL, + handling: .browsing + ).responseString.then { + responseContent in + let bowl = try SwiftSoup.parse(responseContent) + let featuredAnime = try bowl.select("ul.List-Animes") + .first() + .tryUnwrap(NineAnimatorError.decodeError("Cannot retrieve featured anime")) + .select("li") + .map { + animeContainer -> AnimeLink in + + let animeArtworkURL = try URL( + string: animeContainer.select("a > figure.Image > img").attr("src"), + relativeTo: self.endpointURL + ) ?? NineAnimator.placeholderArtworkUrl + let animeLink = try URL( + string: animeContainer.select("a").attr("href"), + relativeTo: self.endpointURL + ).tryUnwrap(NineAnimatorError.urlError) + + let animeTitle = try animeContainer.select("a h2.Title").text() + return AnimeLink( + title: animeTitle, + link: animeLink, + image: animeArtworkURL, + source: self + ) + } + /* + let ongoingAnime = try bowl.select("ul.List-Episodes") + .first() + .tryUnwrap(NineAnimatorError.decodeError("Cannot retrieve latest anime")) + .select("li") + .map { + animeContainer -> AnimeLink in + + let animeArtworkURL = try URL( + string: animeContainer.select("a figure.Image > img").attr("src"), + relativeTo: self.endpointURL + ) ?? NineAnimator.placeholderArtworkUrl + + let animeLink = try URL( + string: animeContainer.select("a").attr("href"), + relativeTo: self.endpointURL + ).tryUnwrap(NineAnimatorError.urlError) + + let animeTitle = try animeContainer.select("a h2.Title").text() + + return AnimeLink( + title: animeTitle, + link: animeLink, + image: animeArtworkURL, + source: self + ) + } + */ + + return BasicFeaturedContainer( + featured: featuredAnime, + latest: [] + ) + } + } +} diff --git a/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Search.swift b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Search.swift new file mode 100644 index 00000000..959da0f4 --- /dev/null +++ b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv+Search.swift @@ -0,0 +1,104 @@ +// +// This file is part of the NineAnimator project. +// +// Copyright © 2018-2020 Marcus Zhou. All rights reserved. +// +// NineAnimator is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// NineAnimator is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with NineAnimator. If not, see . +// + +import Foundation +import NineAnimatorCommon +import SwiftSoup + +extension NASourceAnimeFlv { + class SearchAgent: ContentProvider { + var title: String + + var totalPages: Int? { 1 } + var availablePages: Int { _results == nil ? 0 : 1 } + var moreAvailable: Bool { _results == nil } + + weak var delegate: ContentProviderDelegate? + private var parent: NASourceAnimeFlv + private var performingTask: NineAnimatorAsyncTask? + private var _results: [AnimeLink]? + + func links(on page: Int) -> [AnyLink] { + page == 0 ? _results?.map { .anime($0) } ?? [] : [] + } + + func more() { + if performingTask == nil { + // Animehub includes the search title in the URL path, rather than as a URL query param. + let encodedTitle = self.title.replacingOccurrences(of: " ", with: "+") + + performingTask = parent.requestManager.request( + "/browse?q=\(encodedTitle)", + handling: .browsing + ).responseString.then { + [parent] responseContent -> [AnimeLink]? in + try SwiftSoup.parse(responseContent) + .select("ul.List-Animes > li.Anime").compactMap { + animeContainer -> AnimeLink? in + let animeArtworkURL = try URL( + string: animeContainer.select("a > figure.Image > img").attr("src"), + relativeTo: self.parent.endpointURL + ) ?? NineAnimator.placeholderArtworkUrl + + let animeLink = try URL( + string: animeContainer.select("a").attr("href"), + relativeTo: self.parent.endpointURL + ).tryUnwrap(NineAnimatorError.urlError) + + let animeTitle = try animeContainer.select("a > h2.Title").text() + + return AnimeLink( + title: animeTitle, + link: animeLink, + image: animeArtworkURL, + source: parent + ) + } + } .then { + results -> [AnimeLink] in + if results.isEmpty { + throw NineAnimatorError.searchError("No results found") + } else { return results } + } .error { + [weak self] in + guard let self = self else { return } + + // Reset performing task if the error is not 404 + if !($0 is NineAnimatorError.SearchError) { + self.performingTask = nil + } + + self.delegate?.onError($0, from: self) + } .finally { + [weak self] in + guard let self = self else { return } + self._results = $0 + self.delegate?.pageIncoming(0, from: self) + } + } + } + init(_ query: String, withParent parent: NASourceAnimeFlv) { + self.parent = parent + self.title = query + } + } + func search(keyword: String) -> ContentProvider { + SearchAgent(keyword, withParent: self) + } +} diff --git a/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv.swift b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv.swift new file mode 100644 index 00000000..d2c28049 --- /dev/null +++ b/Modules/Sources/NineAnimatorNativeSources/AnimeSources/AnimeFLV/AnimeFlv.swift @@ -0,0 +1,61 @@ +// +// This file is part of the NineAnimator project. +// +// Copyright © 2018-2020 Marcus Zhou. All rights reserved. +// +// NineAnimator is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// NineAnimator is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with NineAnimator. If not, see . +// + +import Foundation +import NineAnimatorCommon + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +class NASourceAnimeFlv: BaseSource, Source, PromiseSource { + var name: String { "animeflv.net" } + + var aliases: [String] { [] } + + #if canImport(UIKit) + var siteLogo: UIImage { #imageLiteral(resourceName: "AnimeFlv Site Icon") } + #elseif canImport(AppKit) + var siteLogo: NSImage { #imageLiteral(resourceName: "AnimeFlv Site Icon") } + #endif + + var siteDescription: String { + "El mejor portal de anime online para latinoamérica, encuentra animes clásicos, animes del momento, animes más populares y mucho más, todo en animeflv, tu fuente de anime diaria." + } + + var preferredAnimeNameVariant: KeyPath { + \.romaji + } + + override var endpoint: String { "https://m.animeflv.net" } + + func suggestProvider(episode: Episode, forServer server: Anime.ServerIdentifier, withServerName name: String) -> VideoProviderParser? { + VideoProviderRegistry.default.provider(for: name) + } + + func link(from url: URL) -> NineAnimatorPromise { + .fail() + } + + override required init(with parent: NineAnimator) { + super.init(with: parent) + } +} diff --git a/Modules/Sources/NineAnimatorNativeSources/NativeSources.swift b/Modules/Sources/NineAnimatorNativeSources/NativeSources.swift index 7f7490fb..3e13313a 100644 --- a/Modules/Sources/NineAnimatorNativeSources/NativeSources.swift +++ b/Modules/Sources/NineAnimatorNativeSources/NativeSources.swift @@ -46,6 +46,7 @@ public enum NativeSources { registry.register(sourceType: NASourceMonosChinos.self) registry.register(sourceType: NASourceAnimeSaturn.self) registry.register(sourceType: NASourceAnimeWorld.self) + registry.register(sourceType: NASourceAnimeFlv.self) // Disabled sources registry.register(sourceType: NASourceWonderfulSubs.self) diff --git a/NineAnimator/Assets.xcassets/AnimeFlv Site Icon.imageset/Contents.json b/NineAnimator/Assets.xcassets/AnimeFlv Site Icon.imageset/Contents.json new file mode 100644 index 00000000..c013f7a0 --- /dev/null +++ b/NineAnimator/Assets.xcassets/AnimeFlv Site Icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "animeflv.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NineAnimator/Assets.xcassets/AnimeFlv Site Icon.imageset/animeflv.png b/NineAnimator/Assets.xcassets/AnimeFlv Site Icon.imageset/animeflv.png new file mode 100644 index 00000000..9aa710fe Binary files /dev/null and b/NineAnimator/Assets.xcassets/AnimeFlv Site Icon.imageset/animeflv.png differ