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