Impressia/Vernissage/Services/HomeTimelineService.swift

188 lines
7.7 KiB
Swift
Raw Normal View History

2023-01-03 14:09:22 +01:00
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
import CoreData
2023-01-10 08:04:25 +01:00
import MastodonKit
2023-01-03 14:09:22 +01:00
2023-01-28 21:09:31 +01:00
/// Service responsible for managing home timeline.
2023-01-21 18:01:17 +01:00
public class HomeTimelineService {
public static let shared = HomeTimelineService()
2023-01-05 11:55:20 +01:00
private init() { }
2023-01-03 14:09:22 +01:00
2023-01-28 21:09:31 +01:00
public func loadOnBottom(for accountData: AccountData) async throws -> Int {
2023-01-03 14:09:22 +01:00
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Get maximimum downloaded stauts id.
2023-01-11 13:16:43 +01:00
let oldestStatus = StatusDataHandler.shared.getMinimumStatus(accountId: accountData.id, viewContext: backgroundContext)
2023-01-03 14:09:22 +01:00
guard let oldestStatus = oldestStatus else {
2023-01-11 13:16:43 +01:00
return 0
2023-01-03 14:09:22 +01:00
}
2023-01-28 21:09:31 +01:00
let newStatuses = try await self.load(for: accountData, on: backgroundContext, maxId: oldestStatus.id)
2023-01-20 13:47:38 +01:00
try backgroundContext.save()
2023-01-26 15:10:47 +01:00
return newStatuses.count
2023-01-03 14:09:22 +01:00
}
2023-01-28 21:09:31 +01:00
public func loadOnTop(for accountData: AccountData) async throws {
2023-01-03 14:09:22 +01:00
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
2023-01-28 21:09:31 +01:00
// Refresh/load home timeline (refreshing on top downloads always first 40 items).
// TODO: When Apple introduce good way to show new items without scroll to top then we can change that method.
try await self.refresh(for: accountData, on: backgroundContext)
2023-01-20 13:47:38 +01:00
try backgroundContext.save()
2023-01-03 14:09:22 +01:00
}
2023-01-28 21:09:31 +01:00
public func update(status statusData: StatusData, basedOn status: Status, for accountData: AccountData) async throws -> StatusData? {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Update status data in database.
self.copy(from: status, to: statusData, on: backgroundContext)
try backgroundContext.save()
return statusData
}
public func update(attachment: AttachmentData, withData imageData: Data) {
attachment.data = imageData
self.setExifProperties(in: attachment, from: imageData)
CoreDataHandler.shared.save()
}
private func refresh(for accountData: AccountData, on backgroundContext: NSManagedObjectContext) async throws {
2023-01-03 14:09:22 +01:00
guard let accessToken = accountData.accessToken else {
2023-01-20 13:47:38 +01:00
return
}
// Retrieve statuses from API.
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(limit: 40)
2023-01-28 21:09:31 +01:00
// Retrieve all statuses from database.
2023-01-20 13:47:38 +01:00
let dbStatuses = StatusDataHandler.shared.getAllStatuses(accountId: accountData.id)
2023-01-28 21:09:31 +01:00
// Remove statuses that are not in 40 downloaded once.
2023-01-20 13:47:38 +01:00
var dbStatusesToRemove: [StatusData] = []
for dbStatus in dbStatuses {
if !statuses.contains(where: { status in status.id == dbStatus.id }) {
dbStatusesToRemove.append(dbStatus)
}
}
if !dbStatusesToRemove.isEmpty {
StatusDataHandler.shared.remove(accountId: accountData.id, statuses: dbStatusesToRemove)
}
2023-01-20 16:57:25 +01:00
// Add statuses which are not existing in database, but has been downloaded via API.
var statusesToAdd: [Status] = []
for status in statuses {
2023-01-28 21:09:31 +01:00
if !dbStatuses.contains(where: { statusData in statusData.id == status.id }) {
2023-01-20 16:57:25 +01:00
statusesToAdd.append(status)
}
}
2023-01-28 21:09:31 +01:00
// Save statuses in database.
2023-01-20 16:57:25 +01:00
if !statusesToAdd.isEmpty {
2023-01-28 21:09:31 +01:00
_ = try await self.save(statuses: statusesToAdd, for: accountData, on: backgroundContext)
2023-01-20 16:57:25 +01:00
}
2023-01-20 13:47:38 +01:00
}
2023-01-28 21:09:31 +01:00
private func load(for accountData: AccountData,
on backgroundContext: NSManagedObjectContext,
minId: String? = nil,
maxId: String? = nil
) async throws -> [Status] {
2023-01-20 13:47:38 +01:00
guard let accessToken = accountData.accessToken else {
return []
2023-01-03 14:09:22 +01:00
}
2023-01-04 17:56:01 +01:00
2023-01-03 14:09:22 +01:00
// Retrieve statuses from API.
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 20)
2023-01-20 16:57:25 +01:00
2023-01-28 21:09:31 +01:00
// Save statuses in database.
return try await self.save(statuses: statuses, for: accountData, on: backgroundContext)
2023-01-20 16:57:25 +01:00
}
2023-01-28 21:09:31 +01:00
private func save(statuses: [Status],
for accountData: AccountData,
on backgroundContext: NSManagedObjectContext
) async throws -> [Status] {
guard let dbAccount = AccountDataHandler.shared.getAccountData(accountId: accountData.id, viewContext: backgroundContext) else {
throw DatabaseError.cannotDownloadAccount
}
2023-01-26 15:10:47 +01:00
// Proceed statuses with images only.
let statusesWithImages = statuses.getStatusesWithImagesOnly()
2023-01-04 17:56:01 +01:00
// Save status data in database.
2023-01-26 15:10:47 +01:00
for status in statusesWithImages {
2023-01-20 13:47:38 +01:00
let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext)
2023-01-11 13:16:43 +01:00
statusData.pixelfedAccount = dbAccount
dbAccount.addToStatuses(statusData)
2023-01-26 15:10:47 +01:00
self.copy(from: status, to: statusData, on: backgroundContext)
2023-01-04 17:56:01 +01:00
}
2023-01-26 15:10:47 +01:00
return statusesWithImages
2023-01-05 11:55:20 +01:00
}
2023-01-26 15:10:47 +01:00
private func copy(from status: Status,
to statusData: StatusData,
on backgroundContext: NSManagedObjectContext
) {
2023-01-05 11:55:20 +01:00
statusData.copyFrom(status)
2023-01-04 17:56:01 +01:00
2023-01-26 15:10:47 +01:00
for attachment in status.getAllImageMediaAttachments() {
2023-01-03 14:09:22 +01:00
2023-01-04 17:56:01 +01:00
// Save attachment in database.
let attachmentData = statusData.attachments().first { item in item.id == attachment.id }
2023-01-08 14:50:37 +01:00
?? AttachmentDataHandler.shared.createAttachmnentDataEntity(viewContext: backgroundContext)
2023-01-04 17:56:01 +01:00
2023-01-05 11:55:20 +01:00
attachmentData.copyFrom(attachment)
2023-01-04 17:56:01 +01:00
attachmentData.statusId = statusData.id
2023-01-26 15:10:47 +01:00
2023-01-04 17:56:01 +01:00
if attachmentData.isInserted {
attachmentData.statusRelation = statusData
statusData.addToAttachmentRelation(attachmentData)
2023-01-03 14:09:22 +01:00
}
}
}
2023-01-26 15:10:47 +01:00
private func setExifProperties(in attachmentData: AttachmentData, from imageData: Data) {
// Read exif information.
if let exifProperties = imageData.getExifData() {
if let make = exifProperties.getExifValue("Make"), let model = exifProperties.getExifValue("Model") {
attachmentData.exifCamera = "\(make) \(model)"
}
// "Lens" or "Lens Model"
if let lens = exifProperties.getExifValue("Lens") {
attachmentData.exifLens = lens
}
if let createData = exifProperties.getExifValue("CreateDate") {
attachmentData.exifCreatedDate = createData
}
if let focalLenIn35mmFilm = exifProperties.getExifValue("FocalLenIn35mmFilm"),
let fNumber = exifProperties.getExifValue("FNumber")?.calculateExifNumber(),
let exposureTime = exifProperties.getExifValue("ExposureTime"),
let photographicSensitivity = exifProperties.getExifValue("PhotographicSensitivity") {
attachmentData.exifExposure = "\(focalLenIn35mmFilm)mm, f/\(fNumber), \(exposureTime)s, ISO \(photographicSensitivity)"
}
}
}
2023-01-03 14:09:22 +01:00
}