Skip to content

Commit b8c9baf

Browse files
Toilettraumaclaude
andcommitted
refactor(model): extract shared MediaInfo from Manga/Anime/Image Info
The metadata every domain carries — title, description, dates, tags, series, previews, rating, views, uploader, age/hentai, plus external_ids/original_title/ status where they apply — lived duplicated across MangaInfo, AnimeInfo and ImageContainerInfo. A model-wide change (the recent date-precision and tag-axis work) had to touch all three. Factor those fields into one MediaInfo, embedded as `common` in each Info by composition (not inheritance — the Info types stay aggregates a parser fills with designated initializers, and the Swift bridge imports them the same way). Each Info keeps its typed id and a short domain-specific tail (manga: author/artist/chapters, anime: season/episodes, image: item count). Also harden the two ref() helpers (MangaChapterInfo, AnimeEpisodeInfo) to use designated initializers, so a future field reorder in *Ref fails to compile instead of silently rebinding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bd6a197 commit b8c9baf

6 files changed

Lines changed: 117 additions & 201 deletions

File tree

include/aniparse/anime/AnimeModel.hpp

Lines changed: 5 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -99,27 +99,12 @@ enum class AnimeSeason {
9999
struct AnimeInfo {
100100
/// The source's own numeric id, when it has one. @ref aniparse::invalid_anime_id (0) = it
101101
/// does not, which is not an error. Neither a cross-source identity
102-
/// (@ref external_ids) nor the handle to fetch with (@see AnimeGetter::serialize).
102+
/// (@ref MediaInfo::external_ids) nor the handle to fetch with (@see AnimeGetter::serialize).
103103
AnimeID id = invalid_anime_id;
104104

105-
/// Ids this anime carries on other sites, as the source reports them — the
106-
/// consumer's handle for joining the same work across parsers, including
107-
/// across domains (an anime and its manga are one work to a tracker). Empty
108-
/// when the source knows none. Not this parser's own identity: to address the
109-
/// anime here, use the getter (@see AnimeGetter::serialize). @see ExternalId
110-
std::vector<ExternalId> external_ids;
111-
112-
/// The title to show, in whichever language the source leads with (there is no
113-
/// promise it is English or romanized). Empty only when the source does not name
114-
/// the anime at all.
115-
std::string title;
116-
/// The title in the work's original language/script, when the source carries one
117-
/// AND it differs from @ref title. nullopt = no separate original title, or it is
118-
/// the same string — so a consumer never renders the title twice.
119-
std::optional<std::string> original_title;
120-
/// Synopsis, plain text plus any links the source marked up (not HTML). Empty =
121-
/// the source gives none in this response. @see AttributedText
122-
AttributedText description;
105+
/// The metadata shared with every other domain — title, description, dates, tags,
106+
/// series, previews, rating, external ids, and the rest. @see aniparse::MediaInfo
107+
MediaInfo common;
123108

124109
/// The broadcast season it premiered in. nullopt = the source has no season axis
125110
/// at all; AnimeSeason::Unknown = it has one but states no value for this anime.
@@ -129,41 +114,6 @@ struct AnimeInfo {
129114
/// none — compare against `std::chrono::year{}`, since year 0 is otherwise a
130115
/// well-formed value.
131116
std::chrono::year year{};
132-
/// Airing state (ongoing / released / announced / source-specific). A
133-
/// default-constructed status (empty name) = the source states none, and reads as
134-
/// DefaultAiredStatuses::Other rather than as "released". @see AiredStatus
135-
AiredStatus status;
136-
137-
/// When the anime was last touched on the source (a new episode, an edit).
138-
/// @c nullopt = not stated. Orders items within one source only, since
139-
/// sources differ on what counts as an update.
140-
std::optional<ModelDate> update_time;
141-
/// When it first aired. @c nullopt = the source states no exact date —
142-
/// common, since many sources give only @ref season and @ref year.
143-
std::optional<ModelDate> release_time;
144-
145-
/// Opaque change marker for the whole anime; @see MangaInfo::revision.
146-
std::string revision;
147-
148-
/// The franchise(s)/parent work(s) the source places the anime in, primary first.
149-
/// Empty = it places the anime in none — the anime stands alone or the source has
150-
/// no such axis. Usually one; a franchise-tagging source may list several.
151-
std::vector<Series> series;
152-
153-
/// Poster art and thumbnails, best first (a consumer showing one shows previews
154-
/// front()). Empty = the source offers no artwork. Fetch descriptors, not bytes.
155-
/// @see Image
156-
std::vector<Image> previews;
157-
/// The source's labels for this anime — genres, themes, whatever axes it tags by,
158-
/// flattened into one list. A tag whose @ref Tag::ref is non-empty can be fed back
159-
/// into a search; empty = the source lists none in this response.
160-
std::vector<Tag> tags;
161-
162-
/// Community score, normalized to a 0-10 axis (@see Rating). nullopt = the source
163-
/// publishes no score for this anime, which is not a score of zero.
164-
std::optional<Rating> rating;
165-
/// View/popularity counters. nullopt = the source publishes none. @see ViewStats
166-
std::optional<ViewStats> views;
167117

168118
/// Episodes actually available now (a running airing exposes fewer than
169119
/// planned); absent when the source does not state it.
@@ -174,20 +124,6 @@ struct AnimeInfo {
174124
/// the series, not a per-episode measurement, so it is an estimate for a UI and
175125
/// not a seek/progress bound. nullopt = the source does not state it.
176126
std::optional<std::chrono::minutes> episode_duration;
177-
178-
/// Minimum age the source requires to view the anime, in years. 0 = unrestricted
179-
/// or unstated — an adult work is more reliably detected via @ref is_hentai.
180-
/// @see AgeRestriction
181-
AgeRestriction age_restriction = 0;
182-
183-
/// The account that posted the anime, on sources where content is user-submitted.
184-
/// nullopt = the source has no notion of an uploader (a publisher-side catalog).
185-
std::optional<RelatedUser> uploader;
186-
187-
/// The source marks this anime as adult/pornographic. False = it does not mark it,
188-
/// which is weaker than "safe": sources differ on where the line sits, and one
189-
/// with no adult flag at all leaves this false throughout.
190-
bool is_hentai = false;
191127
};
192128

193129
/**
@@ -270,7 +206,7 @@ struct AnimeEpisodeInfo {
270206
* unchanged — the number alone identifies an episode only on sources that
271207
* leave @ref id empty. Cheap: nothing is fetched, the id is copied.
272208
*/
273-
[[nodiscard]] AnimeEpisodeRef ref() const { return { episode, id }; }
209+
[[nodiscard]] AnimeEpisodeRef ref() const { return { .episode = episode, .id = id }; }
274210
};
275211

276212
/**

include/aniparse/images/ImageModel.hpp

Lines changed: 9 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -93,65 +93,19 @@ struct ImageContainerInfo {
9393
/// @ref aniparse::invalid_image_container_id (0) = the source has no numeric id for it.
9494
ImageContainerID id = invalid_image_container_id;
9595

96-
/// A label to show. Where the source titles its containers this is that title;
97-
/// where it does not — a tag-only catalog whose posts have no name — the parser
98-
/// synthesizes one that is stable for the container (built from its most telling
99-
/// tags, or from its id). So it is never empty, but it is not necessarily
100-
/// something the source itself would display, and it is not an identity.
101-
std::string title;
102-
/// Free text about the container, with any links the source marked up. Empty =
103-
/// the source carries none, which is the norm for single-media containers.
104-
/// @see AttributedText
105-
AttributedText description;
106-
/// The source's labels for the container — on a tag-driven catalog this is the
107-
/// post's whole tag set, which is its primary metadata. Each tag whose
108-
/// @ref Tag::ref is non-empty searches for that tag. Empty = the source lists none.
109-
std::vector<Tag> tags;
110-
111-
/// The account that posted the container. nullopt = the source does not credit an
112-
/// uploader (or does not carry it in this response).
113-
std::optional<RelatedUser> uploader;
114-
/// The franchise(s) the content derives from, where the source tags by them,
115-
/// primary first. Empty = it does not, or the work is original rather than
116-
/// derivative (@see series_original). A booru commonly lists several copyrights.
117-
std::vector<Series> series;
118-
119-
/// Cover/sample images for the container as a whole (grid thumbnails).
120-
/// Empty = the source offers no separate preview; the media itself must be used.
121-
/// These are lower-resolution stand-ins, never the content. @see Image
122-
std::vector<Image> previews;
123-
124-
/// Community score, normalized to a 0-10 axis (@see Rating). nullopt = the source
125-
/// publishes no score for the container — note that a source may score posts
126-
/// without exposing the scale, and then no rating is reported at all.
127-
std::optional<Rating> rating;
128-
/// View counters. nullopt = the source publishes none. @see ViewStats
129-
std::optional<ViewStats> views;
130-
131-
/// When the container was last changed on the source. @c nullopt = not
132-
/// stated. For change detection prefer @ref revision, which is defined for
133-
/// equality comparison; this field is for display.
134-
std::optional<ModelDate> update_time;
135-
/// When the container was posted. @c nullopt = the source does not state it.
136-
std::optional<ModelDate> release_time;
137-
138-
/// Opaque change marker for the whole container; @see MangaInfo::revision.
139-
std::string revision;
140-
141-
/// Minimum age the source requires to view the container, in years. 0 =
142-
/// unrestricted or unstated. A source that grades its content by explicitness
143-
/// maps its adult grades onto this (18) as well as onto @ref is_hentai, so a
144-
/// consumer can gate on either. @see AgeRestriction
145-
AgeRestriction age_restriction = 0;
96+
/// The metadata shared with every other domain — title, description, dates, tags,
97+
/// series, previews, rating, and the rest. An image container leaves the members
98+
/// that have no meaning for it (@ref MediaInfo::external_ids,
99+
/// @ref MediaInfo::original_title, @ref MediaInfo::status) defaulted. Note that
100+
/// @ref MediaInfo::title is never empty here — a tag-only catalog whose posts have
101+
/// no name gets a stable synthesized title (built from its most telling tags, or
102+
/// its id), which is not necessarily something the source itself would display and
103+
/// is not an identity. @see aniparse::MediaInfo
104+
MediaInfo common;
146105

147106
/// How many ImageItems the container holds, when the source states it up
148107
/// front (a booru post is 1); absent when only discoverable by paging items().
149108
std::optional<long> total_items;
150-
151-
/// The source marks this container as adult/pornographic. False = it does not
152-
/// mark it, which is weaker than "safe": a source whose adult grades are coarse
153-
/// (or absent) leaves borderline content unflagged. @see age_restriction
154-
bool is_hentai = false;
155109
};
156110

157111
} // namespace aniparse

include/aniparse/manga/MangaModel.hpp

Lines changed: 5 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -111,90 +111,25 @@ inline constexpr MangaID invalid_manga_id = MangaID{ 0 };
111111
struct MangaInfo {
112112
/// The source's own numeric id, when it has one. @ref aniparse::invalid_manga_id (0) = it
113113
/// does not — which is not an error. This is a display/debugging convenience and
114-
/// a source-local key; it is neither a cross-source identity (@ref external_ids)
114+
/// a source-local key; it is neither a cross-source identity (@ref MediaInfo::external_ids)
115115
/// nor the handle to fetch with (@see MangaGetter::serialize).
116116
MangaID id = invalid_manga_id;
117117

118-
/// Ids this manga carries on other sites, as the source reports them — the
119-
/// consumer's handle for joining the same work across parsers. Empty when the
120-
/// source knows none (most reader sites); a metadata source typically knows at
121-
/// least its MyAnimeList id. Not this parser's own identity: to address the
122-
/// manga here, use the getter (@see MangaGetter::serialize). @see ExternalId
123-
std::vector<ExternalId> external_ids;
124-
125-
/// The title to show, in whichever language the source leads with (a source that
126-
/// carries several picks one; there is no promise it is English or romanized).
127-
/// Empty only when the source does not name the manga at all.
128-
std::string title;
129-
/// The title in the work's original language/script, when the source carries one
130-
/// AND it differs from @ref title. nullopt = the source has no separate original
131-
/// title, or it is the same string — so a consumer never renders the title twice.
132-
std::optional<std::string> original_title;
133-
/// Synopsis, plain text plus any links the source marked up. Empty = the source
134-
/// gives no description here (usual for a search card, and some works simply
135-
/// have none). Not HTML: markup is either flattened into the text or lifted into
136-
/// the attributes. @see AttributedText
137-
AttributedText description;
138-
139-
/// When the manga was last touched on the source (a new chapter, an edit).
140-
/// @c nullopt = the source does not state it. Sources differ on what
141-
/// counts as an update, so this orders items within one source only.
142-
std::optional<ModelDate> update_time;
143-
/// When the manga was first published. @c nullopt = the source does not
144-
/// state it — common when only a year is known.
145-
std::optional<ModelDate> release_time;
146-
/// Publication state (ongoing / released / announced / source-specific). A
147-
/// default-constructed status (empty name) = the source states none, and reads as
148-
/// DefaultAiredStatuses::Other rather than as "released". @see AiredStatus
149-
AiredStatus status;
150-
151-
/// Opaque change marker for the whole manga, filled from the cheapest
152-
/// signal the source exposes (an ETag, an updated-at value, an explicit
153-
/// version, or a composite). Compared only for equality: a changed value
154-
/// means the source reports the content as a different revision. Equality
155-
/// is a cheap "probably unchanged" hint, not a content-integrity guarantee
156-
/// (it does not catch a single re-uploaded chapter or mid-list id drift).
157-
/// Empty = the source exposes no such signal.
158-
std::string revision;
118+
/// The metadata shared with every other domain — title, description, dates, tags,
119+
/// series, previews, rating, external ids, and the rest. @see aniparse::MediaInfo
120+
MediaInfo common;
159121

160122
/// Who wrote it. Its name is empty when the source credits no author (or does not
161123
/// carry the credit in this response); the same person is often credited as both
162124
/// author and artist, in which case both fields name them. @see RelatedUser
163125
RelatedUser author;
164126
/// Who drew it. Empty name = not credited here; see @ref author.
165127
RelatedUser artist;
166-
/// The franchise(s)/parent work(s) the source places the manga in, primary first.
167-
/// Empty = it places the manga in none — the manga stands alone or the source has
168-
/// no such axis. Usually one, but a source that tags by franchise (a doujin's
169-
/// parodies, a booru's copyrights) can list several for one work.
170-
std::vector<Series> series;
171128

172-
/// Cover art and thumbnails, best first (a consumer showing one shows previews
173-
/// front()). Empty = the source offers no artwork; the images are fetch
174-
/// descriptors, not bytes. @see Image
175-
std::vector<Image> previews;
176-
/// The source's labels for this manga — genres, themes, whatever axes it tags by,
177-
/// flattened into one list. A tag whose @ref Tag::ref is non-empty can be fed
178-
/// back into a search; empty = the source lists none in this response.
179-
std::vector<Tag> tags;
180-
181-
/// Community score, normalized to a 0-10 axis (@see Rating). nullopt = the source
182-
/// publishes no score for this manga, which is not the same as a score of zero.
183-
std::optional<Rating> rating;
184-
/// View/popularity counters. nullopt = the source publishes none. @see ViewStats
185-
std::optional<ViewStats> views;
186129
/// The list the authenticated user keeps this manga on (reading, planning, …).
187130
/// nullopt = the request was anonymous, the source has no lists, or the user has
188131
/// not filed this manga — the three are not distinguishable here. @see UserList
189132
std::optional<UserList> user_lists;
190-
/// Minimum age the source requires to view the manga, in years. 0 = unrestricted
191-
/// or unstated — an adult work is more reliably detected via @ref is_hentai and
192-
/// the source's own adult flag. @see AgeRestriction
193-
AgeRestriction age_restriction = 0;
194-
195-
/// The account that posted the manga, on sources where content is user-submitted.
196-
/// nullopt = the source has no notion of an uploader (a publisher-side catalog).
197-
std::optional<RelatedUser> uploader;
198133

199134
/// How many chapters the source claims the manga has, when it says so up front.
200135
/// nullopt = it does not, and the only way to know is to page chapters_info().
@@ -207,11 +142,6 @@ struct MangaInfo {
207142
/// nullopt = the source does not report it. Distinct from @ref aniparse::MangaInfo::total_chapters — a work
208143
/// is a count of chapters OR, when it has none, a count of pages.
209144
std::optional<long> total_pages;
210-
211-
/// The source marks this manga as adult/pornographic. False = it does not mark
212-
/// it, which is a weaker statement than "safe": sources differ on where the line
213-
/// sits, and one that has no adult flag at all leaves this false throughout.
214-
bool is_hentai = false;
215145
};
216146

217147
/**
@@ -299,7 +229,7 @@ struct MangaChapterInfo {
299229
* unchanged — the numbers alone identify a chapter only on sources that
300230
* leave @ref id empty. Cheap: nothing is fetched, the id is copied.
301231
*/
302-
[[nodiscard]] MangaChapterRef ref() const { return { volume, chapter, id }; }
232+
[[nodiscard]] MangaChapterRef ref() const { return { .volume = volume, .chapter = chapter, .id = id }; }
303233
};
304234

305235
/**

0 commit comments

Comments
 (0)