Skip to content

Commit d02d7ff

Browse files
adamayoungclaude
andauthored
✨ Align models, filters, and endpoints with TMDb v3 API (#282)
* ✨ Align models, filters, and endpoints with TMDb v3 API Add missing model fields (TVEpisode, Movie, MovieListItem, TVSeason), expand discover filters from ~15% to comprehensive coverage, add MovieSearchFilter year parameter, and implement v4 token session creation endpoint. Co-Authored-By: Claude Opus 4.6 <[email protected]> * 📝 Add MovieListItem and PrimaryReleaseYearFilter to DocC catalog These public types were missing from the TMDb.md topic sections, making them harder to discover in generated documentation. Co-Authored-By: Claude Opus 4.6 <[email protected]> * ♻️ Use consistent idsQueryItemValue helper and private extensions Extract shared idsQueryItemValue helper in DiscoverTVSeriesRequest to match DiscoverMoviesRequest pattern, and make the movies request extension private for consistency. Co-Authored-By: Claude Opus 4.6 <[email protected]> * 🔧 Improve Codecov config and fix test results upload path - Add coverage status checks (project target: auto, patch target: 80%) - Enable PR comment with condensed layout for coverage summaries - Fix test results upload path: junit.xml -> junit-swift-testing.xml (Swift Testing outputs junit-swift-testing.xml, not junit.xml) Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 3a4b166 commit d02d7ff

42 files changed

Lines changed: 1536 additions & 154 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.codecov.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,19 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: auto
6+
threshold: 1%
7+
patch:
8+
default:
9+
target: 80%
10+
11+
comment:
12+
layout: "condensed_header, condensed_files, condensed_footer"
13+
behavior: default
14+
require_changes: false
15+
require_base: false
16+
require_head: true
17+
118
ignore:
2-
- 'Tests'
19+
- 'Tests'

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ jobs:
166166
&& (needs.changes.outputs.swift == 'true' || github.event_name == 'workflow_dispatch')
167167
uses: codecov/codecov-action@v5
168168
with:
169-
files: ./junit.xml
169+
files: ./junit-swift-testing.xml
170170
report_type: test_results
171171
flags: unit-tests
172172
env:

Sources/TMDb/Domain/APIClient/APIRequestQueryItem.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,25 @@ extension APIRequestQueryItem.Name {
5050
static let withPeople = APIRequestQueryItem.Name("with_people")
5151
static let withOriginalLanguage = APIRequestQueryItem.Name("with_original_language")
5252
static let withGenres = APIRequestQueryItem.Name("with_genres")
53+
static let withoutGenres = APIRequestQueryItem.Name("without_genres")
54+
static let withCompanies = APIRequestQueryItem.Name("with_companies")
55+
static let withKeywords = APIRequestQueryItem.Name("with_keywords")
56+
static let withoutKeywords = APIRequestQueryItem.Name("without_keywords")
57+
static let withNetworks = APIRequestQueryItem.Name("with_networks")
58+
static let withWatchProviders = APIRequestQueryItem.Name("with_watch_providers")
59+
static let withRuntimeGreaterThan = APIRequestQueryItem.Name("with_runtime.gte")
60+
static let withRuntimeLessThan = APIRequestQueryItem.Name("with_runtime.lte")
61+
static let voteAverageGreaterThan = APIRequestQueryItem.Name("vote_average.gte")
62+
static let voteAverageLessThan = APIRequestQueryItem.Name("vote_average.lte")
63+
static let voteCountGreaterThan = APIRequestQueryItem.Name("vote_count.gte")
64+
static let voteCountLessThan = APIRequestQueryItem.Name("vote_count.lte")
65+
static let includeVideo = APIRequestQueryItem.Name("include_video")
5366
static let primaryReleaseDateGreaterThan = APIRequestQueryItem.Name("primary_release_date.gte")
5467
static let primaryReleaseDateLessThan = APIRequestQueryItem.Name("primary_release_date.lte")
68+
static let firstAirDateGreaterThan = APIRequestQueryItem.Name("first_air_date.gte")
69+
static let firstAirDateLessThan = APIRequestQueryItem.Name("first_air_date.lte")
70+
static let airDateGreaterThan = APIRequestQueryItem.Name("air_date.gte")
71+
static let airDateLessThan = APIRequestQueryItem.Name("air_date.lte")
5572

5673
static let apiKey = APIRequestQueryItem.Name("api_key")
5774
static let externalSource = APIRequestQueryItem.Name("external_source")

Sources/TMDb/Domain/Models/Movie.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public struct Movie: Identifiable, Codable, Equatable, Hashable, Sendable {
3737
///
3838
public let originalLanguage: String?
3939

40+
///
41+
/// Origin countries of the movie.
42+
///
43+
public let originCountry: [String]?
44+
4045
///
4146
/// Movie overview.
4247
///
@@ -150,6 +155,7 @@ public struct Movie: Identifiable, Codable, Equatable, Hashable, Sendable {
150155
/// - tagline: Movie tagline.
151156
/// - originalTitle: Original movie title.
152157
/// - originalLanguage: Original language of the movie.
158+
/// - originCountry: Origin countries of the movie.
153159
/// - overview: Movie overview.
154160
/// - runtime: Movie runtime, in minutes.
155161
/// - genres: Movie genres.
@@ -177,6 +183,7 @@ public struct Movie: Identifiable, Codable, Equatable, Hashable, Sendable {
177183
tagline: String? = nil,
178184
originalTitle: String? = nil,
179185
originalLanguage: String? = nil,
186+
originCountry: [String]? = nil,
180187
overview: String? = nil,
181188
runtime: Int? = nil,
182189
genres: [Genre]? = nil,
@@ -203,6 +210,7 @@ public struct Movie: Identifiable, Codable, Equatable, Hashable, Sendable {
203210
self.tagline = tagline
204211
self.originalTitle = originalTitle
205212
self.originalLanguage = originalLanguage
213+
self.originCountry = originCountry
206214
self.overview = overview
207215
self.runtime = runtime
208216
self.genres = genres
@@ -235,6 +243,7 @@ extension Movie {
235243
case tagline
236244
case originalTitle
237245
case originalLanguage
246+
case originCountry
238247
case overview
239248
case runtime
240249
case genres
@@ -270,7 +279,7 @@ extension Movie {
270279
/// - Throws: `DecodingError.keyNotFound` if self does not have an entry for the given key.
271280
/// - Throws: `DecodingError.valueNotFound` if self has a null entry for the given key.
272281
///
273-
public init(from decoder: Decoder) throws {
282+
public init(from decoder: Decoder) throws { // swiftlint:disable:this function_body_length
274283
let container = try decoder.container(keyedBy: CodingKeys.self)
275284
let container2 = try decoder.container(keyedBy: CodingKeys.self)
276285

@@ -281,6 +290,9 @@ extension Movie {
281290
self.originalLanguage = try container.decodeIfPresent(
282291
String.self, forKey: .originalLanguage
283292
)
293+
self.originCountry = try container.decodeIfPresent(
294+
[String].self, forKey: .originCountry
295+
)
284296
self.overview = try container.decodeIfPresent(String.self, forKey: .overview)
285297
self.runtime = try container.decodeIfPresent(Int.self, forKey: .runtime)
286298
self.genres = try container.decodeIfPresent([Genre].self, forKey: .genres)

Sources/TMDb/Domain/Models/MovieListItem.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ public struct MovieListItem: Identifiable, Codable, Equatable, Hashable, Sendabl
3232
///
3333
public let originalLanguage: String
3434

35+
///
36+
/// Origin countries of the movie.
37+
///
38+
public let originCountry: [String]?
39+
3540
///
3641
/// Movie overview.
3742
///
@@ -94,6 +99,7 @@ public struct MovieListItem: Identifiable, Codable, Equatable, Hashable, Sendabl
9499
/// - title: Movie title.
95100
/// - originalTitle: Original movie title.
96101
/// - originalLanguage: Original language of the movie.
102+
/// - originCountry: Origin countries of the movie.
97103
/// - overview: Movie overview.
98104
/// - genreIDs: Movie genre identifiers.
99105
/// - releaseDate: Movie release date.
@@ -110,6 +116,7 @@ public struct MovieListItem: Identifiable, Codable, Equatable, Hashable, Sendabl
110116
title: String,
111117
originalTitle: String,
112118
originalLanguage: String,
119+
originCountry: [String]? = nil,
113120
overview: String,
114121
genreIDs: [Genre.ID],
115122
releaseDate: Date? = nil,
@@ -125,6 +132,7 @@ public struct MovieListItem: Identifiable, Codable, Equatable, Hashable, Sendabl
125132
self.title = title
126133
self.originalTitle = originalTitle
127134
self.originalLanguage = originalLanguage
135+
self.originCountry = originCountry
128136
self.overview = overview
129137
self.genreIDs = genreIDs
130138
self.releaseDate = releaseDate
@@ -146,6 +154,7 @@ extension MovieListItem {
146154
case title
147155
case originalTitle
148156
case originalLanguage
157+
case originCountry
149158
case overview
150159
case genreIDs = "genreIds"
151160
case releaseDate
@@ -179,6 +188,9 @@ extension MovieListItem {
179188
self.title = try container.decode(String.self, forKey: .title)
180189
self.originalTitle = try container.decode(String.self, forKey: .originalTitle)
181190
self.originalLanguage = try container.decode(String.self, forKey: .originalLanguage)
191+
self.originCountry = try container.decodeIfPresent(
192+
[String].self, forKey: .originCountry
193+
)
182194
self.overview = try container.decode(String.self, forKey: .overview)
183195
self.genreIDs = try container.decode([Genre.ID].self, forKey: .genreIDs)
184196

Sources/TMDb/Domain/Models/TVEpisode.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ public struct TVEpisode: Identifiable, Codable, Equatable, Hashable, Sendable {
4242
///
4343
public let airDate: Date?
4444

45+
///
46+
/// Type of episode (e.g., "finale", "standard").
47+
///
48+
public let episodeType: String?
49+
50+
///
51+
/// Episode runtime in minutes.
52+
///
53+
public let runtime: Int?
54+
55+
///
56+
/// Identifier of the parent TV series.
57+
///
58+
public let showID: Int?
59+
4560
///
4661
/// TV episode production code.
4762
///
@@ -84,6 +99,9 @@ public struct TVEpisode: Identifiable, Codable, Equatable, Hashable, Sendable {
8499
/// - seasonNumber: TV episode season number.
85100
/// - overview: TV episode overview.
86101
/// - airDate: TV episode air date.
102+
/// - episodeType: Type of episode.
103+
/// - runtime: Episode runtime in minutes.
104+
/// - showID: Identifier of the parent TV series.
87105
/// - productionCode: TV episode production code.
88106
/// - stillPath: TV episode still image path.
89107
/// - crew: TV episode crew.
@@ -98,6 +116,9 @@ public struct TVEpisode: Identifiable, Codable, Equatable, Hashable, Sendable {
98116
seasonNumber: Int,
99117
overview: String? = nil,
100118
airDate: Date? = nil,
119+
episodeType: String? = nil,
120+
runtime: Int? = nil,
121+
showID: Int? = nil,
101122
productionCode: String? = nil,
102123
stillPath: URL? = nil,
103124
crew: [CrewMember]? = nil,
@@ -111,6 +132,9 @@ public struct TVEpisode: Identifiable, Codable, Equatable, Hashable, Sendable {
111132
self.seasonNumber = seasonNumber
112133
self.overview = overview
113134
self.airDate = airDate
135+
self.episodeType = episodeType
136+
self.runtime = runtime
137+
self.showID = showID
114138
self.productionCode = productionCode
115139
self.stillPath = stillPath
116140
self.crew = crew
@@ -130,6 +154,9 @@ extension TVEpisode {
130154
case seasonNumber
131155
case overview
132156
case airDate
157+
case episodeType
158+
case runtime
159+
case showID = "showId"
133160
case productionCode
134161
case stillPath
135162
case crew
@@ -169,6 +196,9 @@ extension TVEpisode {
169196
return try container2.decodeIfPresent(Date.self, forKey: .airDate)
170197
}()
171198

199+
self.episodeType = try container.decodeIfPresent(String.self, forKey: .episodeType)
200+
self.runtime = try container.decodeIfPresent(Int.self, forKey: .runtime)
201+
self.showID = try container.decodeIfPresent(Int.self, forKey: .showID)
172202
self.productionCode = try container.decodeIfPresent(String.self, forKey: .productionCode)
173203
self.stillPath = try container.decodeIfPresent(URL.self, forKey: .stillPath)
174204
self.crew = try container.decodeIfPresent([CrewMember].self, forKey: .crew)

Sources/TMDb/Domain/Models/TVSeason.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ public struct TVSeason: Identifiable, Codable, Equatable, Hashable, Sendable {
4444
///
4545
public let posterPath: URL?
4646

47+
///
48+
/// Average vote score.
49+
///
50+
public let voteAverage: Double?
51+
4752
///
4853
/// Episodes in this TV season.
4954
///
@@ -59,6 +64,7 @@ public struct TVSeason: Identifiable, Codable, Equatable, Hashable, Sendable {
5964
/// - overview: Overview of TV season.
6065
/// - airDate: TV season's air date.
6166
/// - posterPath: TV season's poster path.
67+
/// - voteAverage: Average vote score.
6268
/// - episodes: Episodes in this TV season.
6369
///
6470
public init(
@@ -68,6 +74,7 @@ public struct TVSeason: Identifiable, Codable, Equatable, Hashable, Sendable {
6874
overview: String? = nil,
6975
airDate: Date? = nil,
7076
posterPath: URL? = nil,
77+
voteAverage: Double? = nil,
7178
episodes: [TVEpisode]? = nil
7279
) {
7380
self.id = id
@@ -76,6 +83,7 @@ public struct TVSeason: Identifiable, Codable, Equatable, Hashable, Sendable {
7683
self.overview = overview
7784
self.airDate = airDate
7885
self.posterPath = posterPath
86+
self.voteAverage = voteAverage
7987
self.episodes = episodes
8088
}
8189

@@ -90,6 +98,7 @@ extension TVSeason {
9098
case overview
9199
case airDate
92100
case posterPath
101+
case voteAverage
93102
case episodes
94103
}
95104

@@ -124,6 +133,7 @@ extension TVSeason {
124133
}()
125134

126135
self.posterPath = try container.decodeIfPresent(URL.self, forKey: .posterPath)
136+
self.voteAverage = try container.decodeIfPresent(Double.self, forKey: .voteAverage)
127137
self.episodes = try container.decodeIfPresent([TVEpisode].self, forKey: .episodes)
128138
}
129139

Sources/TMDb/Domain/Services/Authentication/AuthenticationService.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ public protocol AuthenticationService: Sendable {
8888
///
8989
func createSession(withCredential credential: Credential) async throws -> Session
9090

91+
///
92+
/// Creates a TMDb session from a v4 access token.
93+
///
94+
/// Use this method if you already have a v4 access token and want to
95+
/// create a v3 session from it.
96+
///
97+
/// [TMDb API - Authentication: Create Session (from v4 access
98+
/// token)](https://developer.themoviedb.org/reference/authentication-create-session-from-v4-token)
99+
///
100+
/// - Parameter v4AccessToken: A v4 access token.
101+
///
102+
/// - Throws: TMDb error ``TMDbError``.
103+
///
104+
/// - Returns: A TMDb session.
105+
///
106+
func createSession(withV4AccessToken v4AccessToken: String) async throws -> Session
107+
91108
///
92109
/// Deletes a user's session on TMDb.
93110
///
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// CreateSessionFromV4AccessTokenRequest.swift
3+
// TMDb
4+
//
5+
// Copyright © 2026 Adam Young.
6+
//
7+
8+
import Foundation
9+
10+
final class CreateSessionFromV4AccessTokenRequest: CodableAPIRequest<
11+
CreateSessionFromV4AccessTokenRequest.Body, Session
12+
> {
13+
14+
init(accessToken: String) {
15+
let path = "/authentication/session/convert/4"
16+
let body = CreateSessionFromV4AccessTokenRequest.Body(
17+
accessToken: accessToken
18+
)
19+
20+
super.init(path: path, body: body)
21+
}
22+
23+
}
24+
25+
extension CreateSessionFromV4AccessTokenRequest {
26+
27+
struct Body: Encodable, Equatable {
28+
29+
let accessToken: String
30+
31+
}
32+
33+
}

Sources/TMDb/Domain/Services/Authentication/TMDbAuthenticationService.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ final class TMDbAuthenticationService: AuthenticationService {
6666
return session
6767
}
6868

69+
func createSession(withV4AccessToken v4AccessToken: String) async throws -> Session {
70+
let request = CreateSessionFromV4AccessTokenRequest(
71+
accessToken: v4AccessToken
72+
)
73+
74+
let session: Session
75+
do {
76+
session = try await apiClient.perform(request)
77+
} catch let error {
78+
throw TMDbError(error: error)
79+
}
80+
81+
return session
82+
}
83+
6984
func createSession(withCredential credential: Credential) async throws -> Session {
7085
let token = try await requestToken()
7186

0 commit comments

Comments
 (0)