class MediaListItem(ModelMixin, db.Model): """ Database model for media list items. This model maps MediaLists and MediaUserStates """ __tablename__ = "media_list_items" """ The name of the database table """ __table_args__ = (db.UniqueConstraint("media_list_id", "media_user_state_id", name="unique_media_list_item"), ) """ Makes sure that objects that should be unique are unique """ def __init__(self, *args, **kwargs): """ Initializes the Model :param args: The constructor arguments :param kwargs: The constructor keyword arguments """ super().__init__(*args, **kwargs) media_list_id: int = db.Column(db.Integer, db.ForeignKey("media_lists.id"), nullable=False) """ The ID of the media list this list item is a part of """ media_list: MediaList = db.relationship("MediaList", back_populates="media_list_items") """ The media list this list item is a part of """ media_user_state_id: int = db.Column(db.Integer, db.ForeignKey("media_user_states.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False) """ The ID of the media user state this list item references """ media_user_state: MediaUserState = db.relationship( "MediaUserState", backref=db.backref("media_list_items", lazy=True, cascade="all,delete")) """ The media user state this list item references """ @property def identifier_tuple(self) -> Tuple[int, int]: """ :return: A tuple that uniquely identifies this database entry """ return self.media_list_id, self.media_user_state_id def update(self, new_data: "MediaListItem"): """ Updates the data in this record based on another object :param new_data: The object from which to use the new values :return: None """ self.media_list_id = new_data.media_list_id self.media_user_state_id = new_data.media_user_state_id
class MediaUserState(ModelMixin, db.Model): """ Database model that keeps track of a user's entries on external services for a media item """ __tablename__ = "media_user_states" """ The name of the database table """ __table_args__ = (db.UniqueConstraint("media_id_id", "user_id", name="unique_media_user_state"), ) """ Makes sure that objects that should be unique are unique """ def __init__(self, *args, **kwargs): """ Initializes the Model :param args: The constructor arguments :param kwargs: The constructor keyword arguments """ super().__init__(*args, **kwargs) media_id_id: int = db.Column(db.Integer, db.ForeignKey("media_ids.id"), nullable=False) """ The ID of the media ID referenced by this user state """ media_id: MediaId = db.relationship("MediaId", back_populates="media_user_states") """ The media ID referenced by this user state """ user_id: int = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False) """ The ID of the user associated with this user state """ user: User = db.relationship("User", backref=db.backref("media_user_states", lazy=True, cascade="all,delete")) """ The user associated with this user state """ progress: Optional[int] = db.Column(db.Integer, nullable=True) """ The user's current progress consuming the media item """ volume_progress: Optional[int] = db.Column(db.Integer, nullable=True) """ The user's current 'volume' progress. """ score: Optional[int] = db.Column(db.Integer, nullable=True) """ The user's score for the references media item """ consuming_state: ConsumingState \ = db.Column(db.Enum(ConsumingState), nullable=False) """ The current consuming state of the user for this media item """ media_notification: Optional["MediaNotification"] = db.relationship( "MediaNotification", uselist=False, back_populates="media_user_state", cascade="all, delete") """ Notification object for this user state """ @property def identifier_tuple(self) -> Tuple[int, int]: """ :return: A tuple that uniquely identifies this database entry """ return self.media_id_id, self.user_id def update(self, new_data: "MediaUserState"): """ Updates the data in this record based on another object :param new_data: The object from which to use the new values :return: None """ self.media_id_id = new_data.media_id_id self.user_id = new_data.user_id self.progress = new_data.progress self.volume_progress = new_data.volume_progress self.score = new_data.score self.consuming_state = new_data.consuming_state
class ServiceUsername(ModelMixin, db.Model): """ Database model that stores an external service username for a user """ __tablename__ = "service_usernames" """ The name of the database table """ __table_args__ = (db.UniqueConstraint("user_id", "username", "service", name="unique_service_username"), ) """ Makes sure that objects that should be unique are unique """ def __init__(self, *args, **kwargs): """ Initializes the Model :param args: The constructor arguments :param kwargs: The constructor keyword arguments """ super().__init__(*args, **kwargs) user_id: int = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=False) """ The ID of the user associated with this service username """ user: User = db.relationship("User", backref=db.backref("service_usernames", lazy=True, cascade="all,delete")) """ The user associated with this service username """ username: str = db.Column(db.String(255), nullable=False) """ The service username """ service: ListService = db.Column(db.Enum(ListService), nullable=False) """ The external service this item is a username for """ @property def identifier_tuple(self) -> Tuple[int, str, ListService]: """ :return: A tuple that uniquely identifies this database entry """ return self.user_id, self.username, self.service def update(self, new_data: "ServiceUsername"): """ Updates the data in this record based on another object :param new_data: The object from which to use the new values :return: None """ self.user_id = new_data.user_id self.username = new_data.username self.service = new_data.service
class MediaId(ModelMixin, db.Model): """ Database model for media IDs. These are used to map media items to their corresponding external IDS on external sites. """ __tablename__ = "media_ids" """ The name of the database table """ __table_args__ = ( db.UniqueConstraint("media_item_id", "service", "media_type", name="unique_media_item_service_id"), db.UniqueConstraint("media_type", "service", "service_id", name="unique_service_id"), db.ForeignKeyConstraint( ["media_item_id", "media_type", "media_subtype"], [ "media_items.id", "media_items.media_type", "media_items.media_subtype" ]), ) """ Makes sure that objects that should be unique are unique """ def __init__(self, *args, **kwargs): """ Initializes the Model :param args: The constructor arguments :param kwargs: The constructor keyword arguments """ super().__init__(*args, **kwargs) media_item_id: int = db.Column(db.Integer, nullable=False) """ The ID of the media item referenced by this ID """ media_item: MediaItem = db.relationship("MediaItem", back_populates="media_ids") """ The media item referenced by this ID """ media_type: MediaType = db.Column(db.Enum(MediaType), nullable=False) """ The media type of the list item """ media_subtype: MediaSubType = \ db.Column(db.Enum(MediaSubType), nullable=False) """ The media subtype of the list item """ service_id: str = db.Column(db.String(255), nullable=False) """ The ID of the media item on the external service """ service: ListService = db.Column(db.Enum(ListService), nullable=False) """ The service for which this object represents an ID """ media_user_states: List["MediaUserState"] = db.relationship( "MediaUserState", back_populates="media_id", cascade="all, delete") """ Media user states associated with this media ID """ chapter_guess: Optional["MangaChapterGuess"] = db.relationship( "MangaChapterGuess", uselist=False, back_populates="media_id", cascade="all, delete") """ Chapter Guess for this media ID (Only applicable if this is a manga title) """ @property def service_url(self) -> str: """ :return: The URL to the series for the given service """ url_format = list_service_url_formats[self.service] url = url_format \ .replace("@{media_type}", f"{self.media_type.value}") \ .replace("@{id}", self.service_id) return url @property def service_icon(self) -> str: """ :return: The path to the service's icon file """ return url_for( "static", filename=f"images/service_logos/{self.service.value}.png") @property def identifier_tuple(self) -> Tuple[MediaType, ListService, str]: """ :return: A tuple that uniquely identifies this database entry """ return self.media_type, self.service, self.service_id def update(self, new_data: "MediaId"): """ Updates the data in this record based on another object :param new_data: The object from which to use the new values :return: None """ self.media_item_id = new_data.media_item_id self.media_type = new_data.media_type self.service = new_data.service self.service_id = new_data.service_id
class MediaList(ModelMixin, db.Model): """ Database model for user-specific media lists. """ __tablename__ = "media_lists" """ The name of the database table """ __table_args__ = ( db.UniqueConstraint( "name", "user_id", "service", "media_type", name="unique_media_list" ), ) """ Makes sure that objects that should be unique are unique """ def __init__(self, *args, **kwargs): """ Initializes the Model :param args: The constructor arguments :param kwargs: The constructor keyword arguments """ super().__init__(*args, **kwargs) user_id: int = db.Column( db.Integer, db.ForeignKey( "users.id", ondelete="CASCADE", onupdate="CASCADE" ), nullable=False ) """ The ID of the user associated with this list """ user: User = db.relationship( "User", backref=db.backref("media_lists", lazy=True, cascade="all,delete") ) """ The user associated with this list """ name: str = db.Column(db.Unicode(255), nullable=False) """ The name of this list """ service: ListService = db.Column(db.Enum(ListService), nullable=False) """ The service for which this list applies to """ media_type: MediaType = db.Column(db.Enum(MediaType), nullable=False) """ The media type for this list """ media_list_items: List["MediaListItem"] = db.relationship( "MediaListItem", back_populates="media_list", cascade="all, delete" ) """ Media List Items that are a part of this media list """ @property def identifier_tuple(self) -> Tuple[str, int, ListService, MediaType]: """ :return: A tuple that uniquely identifies this database entry """ return self.name, self.user_id, self.service, self.media_type def update(self, new_data: "MediaList"): """ Updates the data in this record based on another object :param new_data: The object from which to use the new values :return: None """ self.user_id = new_data.user_id self.name = new_data.name self.service = new_data.service self.media_type = new_data.media_type
class MediaItem(ModelMixin, db.Model): """ Database model for media items. These model a generic, site-agnostic representation of a series. """ __tablename__ = "media_items" """ The name of the database table """ __table_args__ = ( db.UniqueConstraint("media_type", "media_subtype", "romaji_title", name="unique_media_item_data"), db.UniqueConstraint("id", "media_type", "media_subtype", name="unique_media_item"), ) """ Makes sure that objects that should be unique are unique """ def __init__(self, *args, **kwargs): """ Initializes the Model :param args: The constructor arguments :param kwargs: The constructor keyword arguments """ super().__init__(*args, **kwargs) media_type: MediaType = db.Column(db.Enum(MediaType), nullable=False) """ The media type of the list item """ media_subtype: MediaSubType = db.Column(db.Enum(MediaSubType), nullable=False) """ The subtype (for example, TV short, movie oneshot etc) """ english_title: Optional[str] = db.Column(db.Unicode(255), nullable=True) """ The English title of the media item """ romaji_title: str = db.Column(db.Unicode(255), nullable=False) """ The Japanese title of the media item written in Romaji """ cover_url: str = db.Column(db.String(255), nullable=False) """ An URL to a cover image of the media item """ latest_release: Optional[int] = db.Column(db.Integer, nullable=True) """ The latest release chapter/episode for this media item """ latest_volume_release: Optional[int] = db.Column(db.Integer, nullable=True) """ The latest volume for this media item """ next_episode: Optional[int] = db.Column(db.Integer, nullable=True) """ The next episode to air """ next_episode_airing_time: Optional[int] = \ db.Column(db.Integer, nullable=True) """ The time the next episode airs """ releasing_state: ReleasingState = db.Column(db.Enum(ReleasingState), nullable=False) """ The current releasing state of the media item """ media_ids: List["MediaId"] = db.relationship("MediaId", back_populates="media_item", cascade="all, delete") """ Media IDs associated with this Media item """ ln_releases: List["LnRelease"] = db.relationship( "LnRelease", back_populates="media_item", cascade="all, delete") """ Light novel releases associated with this Media item """ @property def current_release(self) -> Optional[int]: """ The most current release, specifically tailored to the type of media :return: None """ if self.next_episode is not None: return self.next_episode - 1 elif self.latest_volume_release is not None: return self.latest_volume_release elif self.latest_release is not None: return self.latest_release else: return None @property def media_id_mapping(self) -> Dict[ListService, "MediaId"]: """ :return: A dictionary mapping list services to IDs for this media item """ return {x.service: x for x in self.media_ids} @property def identifier_tuple(self) -> Tuple[str, MediaType, MediaSubType]: """ :return: A tuple that uniquely identifies this database entry """ return self.romaji_title, self.media_type, self.media_subtype def update(self, new_data: "MediaItem"): """ Updates the data in this record based on another object :param new_data: The object from which to use the new values :return: None """ self.media_type = new_data.media_type self.media_subtype = new_data.media_subtype self.english_title = new_data.english_title self.romaji_title = new_data.romaji_title self.cover_url = new_data.cover_url self.latest_release = new_data.latest_release self.latest_volume_release = new_data.latest_volume_release self.releasing_state = new_data.releasing_state self.next_episode = new_data.next_episode self.next_episode_airing_time = new_data.next_episode_airing_time @property def title(self) -> str: """ :return: The default title for the media item. """ if self.english_title is None: return self.romaji_title else: return self.english_title @property def own_url(self) -> str: """ :return: The URL to the item's page on the otaku-info site """ return url_for("media.media", media_item_id=self.id) @property def next_episode_datetime(self) -> Optional[datetime]: """ :return: The datetime for when the next episode airs """ if self.next_episode_airing_time is None: return None else: return datetime.fromtimestamp(self.next_episode_airing_time)