Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

Add Support for Reverse Image Searching #272

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//
// 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 <http://www.gnu.org/licenses/>.
//

import Alamofire
import NineAnimatorCommon
import UIKit

public class TraceMoe {
var apiURL: URL { URL(string: "https://api.trace.moe")! }

let requestManager = NAEndpointRelativeRequestManager()

public init() {}

public func search(with image: UIImage) -> NineAnimatorPromise<[TraceMoeSearchResult]> {
NineAnimatorPromise<Data>.firstly {
guard let jpegData = image.jpegData(compressionQuality: 0.8) else {
throw NineAnimatorError.unknownError("Could not convert image to jpeg.")
}
return jpegData
} .thenPromise {
jpegData -> NineAnimatorPromise<TraceMoeSearchResponse> in
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase

let request = self.requestManager.request { session in
return self.requestManager.session.upload(
multipartFormData: { form in
form.append(jpegData, withName: "image", fileName: "image", mimeType: "image/jpeg")
},
to: self.apiURL.appendingPathComponent("/search").absoluteString + "?anilistInfo"
)
}

return request
.onReceivingResponse { try self.validateResponse($0) }
.responseDecodable(type: TraceMoeSearchResponse.self, decoder: jsonDecoder)
} .then {
responseObject in
let filteredResponses = responseObject.result.filter { $0.similarity >= 0.6 }
guard !filteredResponses.isEmpty else { throw NineAnimatorError.searchError("No Results Found") }
return filteredResponses
}
}

public func search(with imageURL: URL) -> NineAnimatorPromise<[TraceMoeSearchResult]> {
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
return self.requestManager.request(
url: self.apiURL.appendingPathComponent("/search"),
method: .post,
query: [
"url": imageURL.absoluteString,
"anilistInfo": ""
]
)
.onReceivingResponse { try self.validateResponse($0) }
.responseDecodable(type: TraceMoeSearchResponse.self, decoder: jsonDecoder)
.then {
responseObject in
let filteredResponses = responseObject.result.filter { $0.similarity >= 0.6 }
guard !filteredResponses.isEmpty else { throw NineAnimatorError.searchError("No Results Found") }
return filteredResponses
}
}

private func validateResponse(_ response: NARequestManager.Response) throws {
guard let HTTPresponse = response.response else { return }
switch HTTPresponse.statusCode {
case 400:
throw NineAnimatorError.searchError("Image Was Not Provided")
case 413:
throw NineAnimatorError.searchError("Your image was above 10MB")
case 429:
throw NineAnimatorError.searchError("You are hitting the rate limiter. Calm down!")
case 500..<600:
if let data = response.data, let errorString = String(data: data, encoding: .utf8) {
throw NineAnimatorError.searchError("Trace.moe experienced a backend error: \(errorString)")
}
throw NineAnimatorError.searchError("Trace.moe experienced a backend error")
default: break
}
}
}

// MARK: - Data Types
public extension TraceMoe {
struct TraceMoeSearchResponse: Codable {
public let result: [TraceMoeSearchResult]
}

struct TraceMoeSearchResult: Codable {
public let from, to: Double
public let anilist: TraceMoeAnilistInfo
public let filename: String
public let video: URL
public let image: URL
public let episode: TraceMoeEpisode?
public let similarity: Double
}

struct TraceMoeAnilistInfo: Codable {
public let id, idMal: Int
public let title: TraceMoeAnilistTitle
public let synonyms: [String]
public let isAdult: Bool
}

struct TraceMoeAnilistTitle: Codable {
public let romaji: String?
public let english: String?
public let native: String?
public let userPreferred: String?
}

/// Abstracts Trace.moe's custom episode type to a string value type
struct TraceMoeEpisode: Codable {
public let value: String

public init(_ value: String) {
self.value = value
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let str = try? container.decode(String.self) {
value = str
} else if let int = try? container.decode(Int.self) {
value = String(int)
} else if let double = try? container.decode(Double.self) {
value = String(double)
} else if let array = try? container.decode([Int].self) {
value = array.description
} else {
throw DecodingError.typeMismatch(String.self, .init(codingPath: decoder.codingPath, debugDescription: "Could not convert trace.moe episode into String"))
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
}
24 changes: 24 additions & 0 deletions NineAnimator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,11 @@
52CADD6721BD86F40077BEB1 /* UITableView+DeselectRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CADD6621BD86F40077BEB1 /* UITableView+DeselectRows.swift */; };
BF6820E326817C93004DCDBD /* NineAnimatorCommon in Frameworks */ = {isa = PBXBuildFile; productRef = BF6820E226817C93004DCDBD /* NineAnimatorCommon */; };
BF6820E426817C93004DCDBD /* NineAnimatorCommon in Embed Frameworks */ = {isa = PBXBuildFile; productRef = BF6820E226817C93004DCDBD /* NineAnimatorCommon */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
BF8CC475268562AB00A0A485 /* ImageSearchSelectorScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CC474268562AB00A0A485 /* ImageSearchSelectorScene.swift */; };
BF8CC4772685646100A0A485 /* ImageSearchSelectorView+PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CC4762685646100A0A485 /* ImageSearchSelectorView+PhotoPicker.swift */; };
BF9203DC25E07E6E00CB2D91 /* AudioBackgroundController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9203DB25E07E6E00CB2D91 /* AudioBackgroundController.swift */; };
BF99DE63268698240019BFC6 /* ImageSearchResultsView+AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF99DE62268698240019BFC6 /* ImageSearchResultsView+AVPlayer.swift */; };
BF99DE65268698920019BFC6 /* ImageSearchResultsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF99DE64268698920019BFC6 /* ImageSearchResultsScene.swift */; };
BFF95F0A257C2DA8001F8484 /* SettingsSceneController+DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF95F09257C2DA8001F8484 /* SettingsSceneController+DocumentPicker.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -563,7 +567,11 @@
2CF8912121DB39A1006D4799 /* icon_rendered.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_rendered.png; sourceTree = "<group>"; };
52CADD6621BD86F40077BEB1 /* UITableView+DeselectRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+DeselectRows.swift"; sourceTree = "<group>"; };
52F6A4F221DABDB00005D78D /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = SOURCE_ROOT; };
BF8CC474268562AB00A0A485 /* ImageSearchSelectorScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSearchSelectorScene.swift; sourceTree = "<group>"; };
BF8CC4762685646100A0A485 /* ImageSearchSelectorView+PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageSearchSelectorView+PhotoPicker.swift"; sourceTree = "<group>"; };
BF9203DB25E07E6E00CB2D91 /* AudioBackgroundController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioBackgroundController.swift; sourceTree = "<group>"; };
BF99DE62268698240019BFC6 /* ImageSearchResultsView+AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImageSearchResultsView+AVPlayer.swift"; sourceTree = "<group>"; };
BF99DE64268698920019BFC6 /* ImageSearchResultsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSearchResultsScene.swift; sourceTree = "<group>"; };
BFF95F09257C2DA8001F8484 /* SettingsSceneController+DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsSceneController+DocumentPicker.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -895,6 +903,7 @@
2C4ABC7721B59C95009B4D47 /* Views */ = {
isa = PBXGroup;
children = (
BF8CC4732685621F00A0A485 /* Image Search Scene */,
2C0DA2E3238B2784002C6447 /* Settings Scene */,
2C665570235CE70300A6126A /* Library Scene */,
2C71DEC1226CF748004F3AC3 /* Server Selection Scene */,
Expand Down Expand Up @@ -1297,6 +1306,17 @@
path = "User Notification";
sourceTree = "<group>";
};
BF8CC4732685621F00A0A485 /* Image Search Scene */ = {
isa = PBXGroup;
children = (
BF8CC474268562AB00A0A485 /* ImageSearchSelectorScene.swift */,
BF8CC4762685646100A0A485 /* ImageSearchSelectorView+PhotoPicker.swift */,
BF99DE62268698240019BFC6 /* ImageSearchResultsView+AVPlayer.swift */,
BF99DE64268698920019BFC6 /* ImageSearchResultsScene.swift */,
);
path = "Image Search Scene";
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -1658,12 +1678,14 @@
2C71DEC5226E0610004F3AC3 /* ServerSelectionView.swift in Sources */,
2CA1DD5C21E2B404009241CB /* HomeController.swift in Sources */,
2C90C6A6236F98C600C18620 /* LibraryTrackingReferenceCell.swift in Sources */,
BF8CC4772685646100A0A485 /* ImageSearchSelectorView+PhotoPicker.swift in Sources */,
2C11187125B624F500BD49D2 /* CoreExportsLogging.swift in Sources */,
2C90C6A4236F988100C18620 /* LibraryUntrackedReferenceCell.swift in Sources */,
2CB004252200AEBC00351B7E /* OfflineContent.swift in Sources */,
2C96DAE424562A2F007A29B0 /* InformationSceneAiringEpisodeCell.swift in Sources */,
2CAA70D321CFF42A00F0D082 /* NativePlayerController.swift in Sources */,
2C368A0D2592F0580069DE91 /* CoreExportsNineAnimator+Fetch.swift in Sources */,
BF8CC475268562AB00A0A485 /* ImageSearchSelectorScene.swift in Sources */,
2C9383CD21FBF442008C0D01 /* UIView+MakeThemable.swift in Sources */,
2C1C717322186EC900760A69 /* TrackingServiceTableViewController.swift in Sources */,
2C93BD43221E1FE8000411CB /* InformationSceneStatisticsTableViewCell.swift in Sources */,
Expand Down Expand Up @@ -1692,6 +1714,7 @@
2C85367E222EEC7400A5CFB9 /* RecentsSceneCollectionCollectionViewCell.swift in Sources */,
2CB9919421FEAC5D0055B7A7 /* DetailedEpisodeTableViewCell.swift in Sources */,
2C86C1EB225D6F9F00950ABF /* SetupWelcomeViewController.swift in Sources */,
BF99DE65268698920019BFC6 /* ImageSearchResultsScene.swift in Sources */,
2C66779821BC2213000E5ACC /* SearchViewController.swift in Sources */,
2CB4AF96239AD1B700EE4EDA /* CachableAVAssetLoaderDelegate.swift in Sources */,
2C9C44FE21CD2B33004C8F0C /* SettingsSceneController.swift in Sources */,
Expand All @@ -1706,6 +1729,7 @@
52CADD6721BD86F40077BEB1 /* UITableView+DeselectRows.swift in Sources */,
2C12EDDE21BDE6B20064D2BD /* HalfFillTransitionDelegate.swift in Sources */,
2CE18B0C2374839100771A16 /* LibrarySubscriptionCell.swift in Sources */,
BF99DE63268698240019BFC6 /* ImageSearchResultsView+AVPlayer.swift in Sources */,
2C9383D421FCCC43008C0D01 /* UILabel+StyleAttributes.swift in Sources */,
2C35C968227F223700D469E6 /* CalendarHeaderView.swift in Sources */,
2C12EDDB21BDE6B20064D2BD /* HalfFillViewControllerProtocol.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class AnimeScheduleCollectionViewController: MinFilledCollectionViewController,
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let view = collectionView.cellForItem(at: indexPath) as? CalendarAnimeCell,
let item = view.representingScheduledAnime {
RootViewController.shared?.open(immedietly: item.link, in: self)
RootViewController.shared?.open(immedietly: item.link, method: .inController(self))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ class AnimeInformationTableViewController: UITableViewController, DontBotherView
let cell = tableView.dequeueReusableCell(withIdentifier: "anime.related", for: indexPath) as! InformationSceneRelatedTableViewCell
cell.initialize(_relatedReferences!) {
[weak self] reference in
RootViewController.shared?.open(immedietly: .listingReference(reference), in: self)
guard let self = self else { return }
RootViewController.shared?.open(immedietly: .listingReference(reference), method: .inController(self))
}
return cell
case .airingSchedule:
Expand Down Expand Up @@ -658,7 +659,7 @@ extension AnimeInformationTableViewController {
/// Open the match directly
private func onPerfectMatch(_ animeLink: AnimeLink) {
if presentingAnimeInformation == nil { navigationController?.popViewController(animated: true) }
RootViewController.shared?.open(immedietly: .anime(animeLink), in: self)
RootViewController.shared?.open(immedietly: .anime(animeLink), method: .inController(self))
}

/// Present options to the user for multiple match
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ extension ContentListViewController {
// Open the link if it exists
if let cell = cell as? ListingEntryTableViewCell,
let link = cell.link {
RootViewController.shared?.open(immedietly: link, in: self)
RootViewController.shared?.open(immedietly: link, method: .inController(self))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ extension LibrarySceneController {
let anime = updatedAnimeLinks.first {
RootViewController.shared?.open(
immedietly: .anime(anime),
in: parent
method: .inController(parent)
)
} else if let category = parent.category(withIdentifier: "library.category.subscribed") {
// Else present the subscription category
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ extension LibrarySceneController {
let anime = cachedRecentlyWatchedList[indexPath.item]
RootViewController.shared?.open(
immedietly: .anime(anime),
in: self
method: .inController(self)
)
case .collection:
let collection = self.collection(atPath: indexPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ extension LibrarySubscriptionCategoryController {

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let link = cachedWatchedAnimeItems[indexPath.item]
RootViewController.shared?.open(immedietly: link, in: self)
RootViewController.shared?.open(immedietly: link, method: .inController(self))
}

override func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
Expand Down
Loading