@@ -23,20 +23,17 @@ private let CACHE_URL = URL(
23
23
fileURLWithPath: " cache " , relativeTo:
24
24
FileManager . default. containerURL ( forSecurityApplicationGroupIdentifier: " group.APOD " ) !)
25
25
26
- public class APODEntry : Decodable {
27
- public var date : YearMonthDay
28
- var remoteImageURL : URL
29
- public var copyright : String ?
30
- public var title : String ?
31
- public var explanation : String ?
32
- public var mediaType : MediaType
33
-
34
- var localDataURL : URL {
35
- CACHE_URL . appendingPathComponent ( date. description) . appendingPathExtension ( DATA_PATH_EXTENSION)
36
- }
37
- var localImageURL : URL {
38
- CACHE_URL . appendingPathComponent ( date. description)
39
- }
26
+ public class APODEntry : Codable {
27
+ private let rawEntry : RawAPODEntry
28
+
29
+ public var date : YearMonthDay { rawEntry. date }
30
+ public var title : String ? { rawEntry. title }
31
+ public var copyright : String ? { rawEntry. copyright }
32
+ public var explanation : String ? { rawEntry. explanation }
33
+
34
+ public let localDataURL : URL
35
+ public let localImageURL : URL
36
+ public let remoteImageURL : URL
40
37
41
38
var PREVIEW_overrideImage : UIImage ?
42
39
private var _loadedImage : UIImage ?
@@ -45,43 +42,43 @@ public class APODEntry: Decodable {
45
42
return _loadedImage
46
43
}
47
44
48
- public enum MediaType {
49
- case image
50
- case video
51
- case unknown( String ? )
52
-
53
- init ( rawValue: String ? ) {
54
- if rawValue == " image " {
55
- self = . image
56
- } else if rawValue == " video " {
57
- self = . video
58
- } else {
59
- self = . unknown( rawValue)
60
- }
45
+ public required init ( from decoder: Decoder ) throws {
46
+ rawEntry = try RawAPODEntry ( from: decoder)
47
+ localDataURL = CACHE_URL . appendingPathComponent ( rawEntry. date. description) . appendingPathExtension ( DATA_PATH_EXTENSION)
48
+ localImageURL = CACHE_URL . appendingPathComponent ( rawEntry. date. description)
49
+
50
+ if let hdurl = rawEntry. hdurl {
51
+ remoteImageURL = hdurl
52
+ } else if let url = rawEntry. url {
53
+ remoteImageURL = url
54
+ } else {
55
+ throw APODErrors . missingURL
61
56
}
62
57
}
63
58
59
+ public func encode( to encoder: Encoder ) throws {
60
+ try rawEntry. encode ( to: encoder)
61
+ }
62
+ }
63
+
64
+ struct RawAPODEntry : Codable {
65
+ var date : YearMonthDay
66
+ var hdurl : URL ?
67
+ var url : URL ?
68
+ var title : String ?
69
+ var copyright : String ?
70
+ var explanation : String ?
71
+ var mediaType : String ?
72
+
64
73
enum CodingKeys : String , CodingKey {
65
74
case copyright
66
75
case date
67
76
case explanation
68
77
case hdurl
69
78
case url
70
- case media_type
71
- case service_version
79
+ case mediaType = " media_type "
72
80
case title
73
81
}
74
-
75
- public required init ( from decoder: Decoder ) throws {
76
- let container = try decoder. container ( keyedBy: CodingKeys . self)
77
- date = try container. decode ( YearMonthDay . self, forKey: . date)
78
- let urlString = try container. decodeIfPresent ( String . self, forKey: . hdurl) ?? container. decode ( String . self, forKey: . url)
79
- remoteImageURL = try URL ( string: urlString) . orThrow ( APODErrors . invalidURL ( urlString) )
80
- copyright = try container. decodeIfPresent ( String . self, forKey: . copyright)
81
- title = try container. decodeIfPresent ( String . self, forKey: . title)
82
- explanation = try container. decodeIfPresent ( String . self, forKey: . explanation)
83
- mediaType = MediaType ( rawValue: try container. decodeIfPresent ( String . self, forKey: . media_type) )
84
- }
85
82
}
86
83
87
84
func _downloadImageIfNeeded( _ entry: APODEntry ) -> AnyPublisher < APODEntry , Error > {
@@ -92,7 +89,18 @@ func _downloadImageIfNeeded(_ entry: APODEntry) -> AnyPublisher<APODEntry, Error
92
89
print ( " Downloading image for \( entry. date) " )
93
90
return URLSession . shared. downloadTaskPublisher ( for: entry. remoteImageURL)
94
91
. tryMap { url in
95
- try FileManager . default. moveItem ( at: url, to: entry. localImageURL)
92
+ print ( " Trying to move \( url) to \( entry. localImageURL) " )
93
+ do {
94
+ try FileManager . default. moveItem ( at: url, to: entry. localImageURL)
95
+ } catch {
96
+ if ( try ? entry. localImageURL. checkResourceIsReachable ( ) ) ?? false {
97
+ // This race should be rare in practice, but happens frequently during development, when a new build
98
+ // is installed in the simulator, and the app and extension both try to fill the cache at the same time.
99
+ print ( " Image already cached for \( entry. date) , continuing " )
100
+ return entry
101
+ }
102
+ throw error
103
+ }
96
104
print ( " Moved downloaded file! " )
97
105
return entry
98
106
}
@@ -108,7 +116,6 @@ public class APODClient {
108
116
private init ( ) {
109
117
do {
110
118
try FileManager . default. createDirectory ( at: CACHE_URL, withIntermediateDirectories: true )
111
-
112
119
for url in try FileManager . default. contentsOfDirectory ( at: CACHE_URL, includingPropertiesForKeys: nil ) where url. pathExtension == DATA_PATH_EXTENSION {
113
120
do {
114
121
let data = try Data ( contentsOf: url)
@@ -135,17 +142,28 @@ public class APODClient {
135
142
136
143
var components = API_URL
137
144
components. queryItems [ withDefault: [ ] ]
138
- . append ( URLQueryItem ( name: " date " , value: YearMonthDay . current. description) )
145
+ . append ( contentsOf: [
146
+ // Rather than requesting the current date, request a range starting from yesterday to avoid "no data available"
147
+ // https://github.com/nasa/apod-api/issues/48
148
+ URLQueryItem ( name: " start_date " , value: ( YearMonthDay . yesterday ?? YearMonthDay . today) . description) ,
149
+ // Including end_date returns 400 when end_date is after today
150
+ ] )
139
151
140
152
return URLSession . shared. dataTaskPublisher ( for: components. url. orFatalError ( " Failed to build API URL " ) )
141
153
. tryMap ( ) { ( data, response) in
142
154
print ( " Got response! \( response) " )
143
- guard ( response as? HTTPURLResponse ) ? . statusCode == 200 else {
155
+ guard let response = response as? HTTPURLResponse else {
144
156
throw URLError ( . badServerResponse)
145
157
}
158
+ guard response. statusCode == 200 else {
159
+ throw APODErrors . failureResponse ( statusCode: response. statusCode)
160
+ }
146
161
147
- let entry = try JSONDecoder ( ) . decode ( APODEntry . self, from: data)
148
- try data. write ( to: entry. localDataURL)
162
+ let entries = try JSONDecoder ( ) . decode ( [ APODEntry ] . self, from: data)
163
+ guard let entry = entries. last else {
164
+ throw APODErrors . emptyResponse
165
+ }
166
+ try JSONEncoder ( ) . encode ( entry) . write ( to: entry. localDataURL)
149
167
return entry
150
168
}
151
169
. flatMap ( _downloadImageIfNeeded)
0 commit comments