···4343/// Map a file extension to its MIME content type using the system type database.
4444public func contentTypeForExtension(_ ext: String) -> String {
4545 if let utType = UTType(filenameExtension: ext),
4646- let mimeType = utType.preferredMIMEType {
4646+ let mimeType = utType.preferredMIMEType
4747+ {
4748 return mimeType
4849 }
4950 return "application/octet-stream"
+2-2
Sources/AtticCore/ImageThumbnailer.swift
···1717 public static func thumbnail(
1818 from data: Data,
1919 maxDimension: Int = 400,
2020- quality: Double = 0.8
2020+ quality: Double = 0.8,
2121 ) throws -> Data {
2222 guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
2323 throw ThumbnailError.decodeFailed("CGImageSourceCreateWithData returned nil")
···4343 data as CFMutableData,
4444 UTType.jpeg.identifier as CFString,
4545 1,
4646- nil
4646+ nil,
4747 ) else {
4848 throw ThumbnailError.decodeFailed("CGImageDestinationCreateWithData failed")
4949 }
+7-8
Sources/AtticCore/ThumbnailService.swift
···2020 cache: ThumbnailCache = ThumbnailCache(),
2121 s3: S3Providing,
2222 dataStore: ViewerDataStore,
2323- maxConcurrent: Int = 6
2323+ maxConcurrent: Int = 6,
2424 ) {
2525 self.cache = cache
2626 self.s3 = s3
···5555 }
56565757 return try await self.generateThumbnail(
5858- for: asset, uuid: uuid, thumbKey: thumbKey
5858+ for: asset, uuid: uuid, thumbKey: thumbKey,
5959 )
6060 }
6161···7676 /// Download original, generate thumbnail, save to cache + S3.
7777 /// Acquires and releases a concurrency slot synchronously within actor context.
7878 private func generateThumbnail(
7979- for asset: AssetView, uuid: String, thumbKey: String
7979+ for asset: AssetView, uuid: String, thumbKey: String,
8080 ) async throws -> Data {
8181 await acquireSlot()
8282 defer { releaseSlot() }
···8888 throw ThumbnailError.s3Failure(uuid, error)
8989 }
90909191- let jpegData: Data
9292- if asset.isVideo {
9393- jpegData = try VideoThumbnailer.thumbnail(from: originalData)
9191+ let jpegData: Data = if asset.isVideo {
9292+ try VideoThumbnailer.thumbnail(from: originalData)
9493 } else {
9595- jpegData = try ImageThumbnailer.thumbnail(from: originalData)
9494+ try ImageThumbnailer.thumbnail(from: originalData)
9695 }
97969897 // Save to local cache
···10099101100 // Best-effort upload to S3 (don't fail if upload errors)
102101 try? await s3.putObject(
103103- key: thumbKey, body: jpegData, contentType: "image/jpeg"
102102+ key: thumbKey, body: jpegData, contentType: "image/jpeg",
104103 )
105104106105 return jpegData
+2-2
Sources/AtticCore/VideoThumbnailer.swift
···1212 static func thumbnail(
1313 from fileURL: URL,
1414 maxDimension: Int = 400,
1515- quality: Double = 0.8
1515+ quality: Double = 0.8,
1616 ) throws -> Data {
1717 let asset = AVURLAsset(url: fileURL)
1818 let generator = AVAssetImageGenerator(asset: asset)
···4343 public static func thumbnail(
4444 from data: Data,
4545 maxDimension: Int = 400,
4646- quality: Double = 0.8
4646+ quality: Double = 0.8,
4747 ) throws -> Data {
4848 let tempURL = FileManager.default.temporaryDirectory
4949 .appendingPathComponent("attic-video-\(UUID().uuidString).mov")
+29-28
Sources/AtticCore/ViewerDataStore.swift
···1717 public init(
1818 uuid: String, filename: String, dateCreated: String?,
1919 year: Int?, albums: [String], isFavorite: Bool,
2020- isVideo: Bool, width: Int, height: Int, s3Key: String
2020+ isVideo: Bool, width: Int, height: Int, s3Key: String,
2121 ) {
2222 self.uuid = uuid
2323 self.filename = filename
···7979 public func load(
8080 manifest: Manifest,
8181 s3: S3Providing,
8282- onProgress: @escaping @Sendable (Int, Int) -> Void = { _, _ in }
8282+ onProgress: @escaping @Sendable (Int, Int) -> Void = { _, _ in },
8383 ) async {
8484 let entries = Array(manifest.entries.values)
8585 _expectedTotal = entries.count
···126126 // Sort by date descending (newest first), nil dates last
127127 assets.sort { a, b in
128128 switch (a.dateCreated, b.dateCreated) {
129129- case let (dateA?, dateB?): return dateA > dateB
130130- case (nil, _): return false
131131- case (_, nil): return true
129129+ case let (dateA?, dateB?): dateA > dateB
130130+ case (nil, _): false
131131+ case (_, nil): true
132132 }
133133 }
134134···156156 favorites: Bool? = nil,
157157 mediaType: String? = nil,
158158 page: Int = 1,
159159- pageSize: Int = 50
159159+ pageSize: Int = 50,
160160 ) -> AssetPage {
161161 let filtered = applyFilters(
162162- year: year, album: album, favorites: favorites, mediaType: mediaType
162162+ year: year, album: album, favorites: favorites, mediaType: mediaType,
163163 )
164164165165 let totalCount = filtered.count
166166 let startIndex = (page - 1) * pageSize
167167 let endIndex = min(startIndex + pageSize, totalCount)
168168 let pageAssets = startIndex < totalCount
169169- ? Array(filtered[startIndex..<endIndex])
169169+ ? Array(filtered[startIndex ..< endIndex])
170170 : []
171171172172 return AssetPage(assets: pageAssets, totalCount: totalCount)
···179179 year: Int? = nil,
180180 album: String? = nil,
181181 favorites: Bool? = nil,
182182- mediaType: String? = nil
182182+ mediaType: String? = nil,
183183 ) -> FilterOptions {
184184 var yearCounts: [Int: Int] = [:]
185185 var albumCounts: [String: Int] = [:]
···190190 let matchesYear = year == nil || asset.year == year
191191 let matchesAlbum = album == nil || asset.albums.contains(album!)
192192 let matchesFav = favorites != true || asset.isFavorite
193193- let matchesType: Bool
194194- switch mediaType {
195195- case "photo": matchesType = !asset.isVideo
196196- case "video": matchesType = asset.isVideo
197197- default: matchesType = true
193193+ let matchesType: Bool = switch mediaType {
194194+ case "photo": !asset.isVideo
195195+ case "video": asset.isVideo
196196+ default: true
198197 }
199198200199 // Year counts: apply all filters except year
201201- if matchesAlbum && matchesFav && matchesType {
200200+ if matchesAlbum, matchesFav, matchesType {
202201 if let y = asset.year { yearCounts[y, default: 0] += 1 }
203202 }
204203 // Album counts: apply all filters except album
205205- if matchesYear && matchesFav && matchesType {
206206- for a in asset.albums { albumCounts[a, default: 0] += 1 }
204204+ if matchesYear, matchesFav, matchesType {
205205+ for a in asset.albums {
206206+ albumCounts[a, default: 0] += 1
207207+ }
207208 }
208209 // Totals: all filters applied
209209- if matchesYear && matchesAlbum && matchesFav && matchesType {
210210+ if matchesYear, matchesAlbum, matchesFav, matchesType {
210211 if asset.isVideo { videoCount += 1 } else { photoCount += 1 }
211212 }
212213 }
···221222 totalVideos: videoCount,
222223 isLoading: _isLoading,
223224 loaded: _loadedCount,
224224- totalInLibrary: _isLoading ? _expectedTotal : assets.count
225225+ totalInLibrary: _isLoading ? _expectedTotal : assets.count,
225226 )
226227 }
227228···233234 // MARK: - Private
234235235236 private func applyFilters(
236236- year: Int?, album: String?, favorites: Bool?, mediaType: String?
237237+ year: Int?, album: String?, favorites: Bool?, mediaType: String?,
237238 ) -> [AssetView] {
238239 var filtered = assets
239240 if let year {
···243244 filtered = filtered.filter { $0.albums.contains(album) }
244245 }
245246 if let favorites, favorites {
246246- filtered = filtered.filter { $0.isFavorite }
247247+ filtered = filtered.filter(\.isFavorite)
247248 }
248249 if let mediaType {
249250 switch mediaType {
250251 case "photo": filtered = filtered.filter { !$0.isVideo }
251251- case "video": filtered = filtered.filter { $0.isVideo }
252252+ case "video": filtered = filtered.filter(\.isVideo)
252253 default: break
253254 }
254255 }
···261262 }
262263263264 private static func assetView(from meta: AssetMetadata) -> AssetView {
264264- let year: Int?
265265- if let dateStr = meta.dateCreated,
266266- let date = try? Date.ISO8601FormatStyle().parse(dateStr) {
267267- year = Calendar.current.component(.year, from: date)
265265+ let year: Int? = if let dateStr = meta.dateCreated,
266266+ let date = try? Date.ISO8601FormatStyle().parse(dateStr)
267267+ {
268268+ Calendar.current.component(.year, from: date)
268269 } else {
269269- year = nil
270270+ nil
270271 }
271272272273 let isVideo = meta.type.map { isVideoUTI($0) } ?? false
···281282 isVideo: isVideo,
282283 width: meta.width,
283284 height: meta.height,
284284- s3Key: meta.s3Key
285285+ s3Key: meta.s3Key,
285286 )
286287 }
287288}