Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MediaTracker · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/bonukai/MediaTracker/blob/main/LICENSE.md) [![Join the chat at https://gitter.im/bonukai/MediaTracker](https://badges.gitter.im/bonukai/MediaTracker.svg)](https://gitter.im/bonukai/MediaTracker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Crowdin](https://badges.crowdin.net/mediatracker/localized.svg)](https://crowdin.com/project/mediatracker) [![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/bonukai/mediatracker)](https://hub.docker.com/r/bonukai/mediatracker) [![Docker Pulls](https://img.shields.io/docker/pulls/bonukai/mediatracker)](https://hub.docker.com/r/bonukai/mediatracker) [![CodeFactor](https://www.codefactor.io/repository/github/bonukai/mediatracker/badge)](https://www.codefactor.io/repository/github/bonukai/mediatracker) [![codecov](https://codecov.io/gh/bonukai/MediaTracker/branch/main/graph/badge.svg?token=CPMW6R7M1Z)](https://codecov.io/gh/bonukai/MediaTracker)

Self hosted platform for tracking movies, tv shows, video games, books and audiobooks, highly inspired by [flox](https://github.com/devfake/flox)
Self hosted platform for tracking movies, tv shows, video games, books, audiobooks and music albums, highly inspired by [flox](https://github.com/devfake/flox)

# Demo

Expand Down Expand Up @@ -150,6 +150,8 @@ docker run -p 7481:7481 mediatracker
| [IGDB](https://www.igdb.com/)\* | video game | ✗ |
| [Audible API](https://audible.readthedocs.io/en/latest/misc/external_api.html) | audiobooks | ✓ |
| [Open Library](https://openlibrary.org/) | books | ✗ |
| [MusicBrainz](https://musicbrainz.org/) | music | ✗ |


\* IGDB has a limit of 4 requests per second. Because of that IGDB API key is not provided with MediaTracker, it can be acquired [here](https://api-docs.igdb.com/#account-creation) and set in [http://localhost:7481/#/settings/configuration](http://localhost:7481/#/settings/configuration)

Expand Down
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "MediaTracker",
"description": "Self hosted media tracker for movies, tv shows, video games, books and audiobooks",
"description": "Self hosted media tracker for movies, tv shows, video games, books, audiobooks, and music albums",
"repository": "https://github.com/bonukai/MediaTracker",
"stack": "container",
"env": {
Expand Down
6 changes: 5 additions & 1 deletion client/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,14 @@ export const MyRouter: FunctionComponent = () => {
path="/audiobooks"
element={<ItemsPage key="/audiobooks" mediaType="audiobook" />}
/>
<Route
path="/music"
element={<ItemsPage key="/music" mediaType="music" />}
/>

<Route
path="/upcoming"
element={<UpcomingPage key="/audiobooks" />}
element={<UpcomingPage key="/upcoming" />}
/>
<Route
path="/watchlist"
Expand Down
277 changes: 140 additions & 137 deletions client/src/components/AddAndRemoveFromSeenHistoryButton.tsx
Original file line number Diff line number Diff line change
@@ -1,137 +1,140 @@
import React, { FunctionComponent } from 'react';
import { plural, Trans } from '@lingui/macro';

import {
MediaItemDetailsResponse,
MediaItemItemsResponse,
TvEpisode,
TvSeason,
} from 'mediatracker-api';
import {
formatEpisodeNumber,
formatSeasonNumber,
isAudiobook,
isBook,
isMovie,
isTvShow,
isVideoGame,
} from 'src/utils';
import { Modal } from 'src/components/Modal';
import { SelectSeenDate } from 'src/components/SelectSeenDate';
import { markAsUnseen } from 'src/api/details';
import { Confirm } from 'src/components/Confirm';

export const AddToSeenHistoryButton: FunctionComponent<{
mediaItem: MediaItemItemsResponse;
season?: TvSeason;
episode?: TvEpisode;
useSeasonAndEpisodeNumber?: boolean;
}> = (props) => {
const { mediaItem, season, episode, useSeasonAndEpisodeNumber } = {
useSeasonAndEpisodeNumber: false,
...props,
};

return (
<>
<Modal
openModal={(openModal) => (
<div className="text-sm btn-blue bg" onClick={openModal}>
{isAudiobook(mediaItem) && <Trans>Add to listened history</Trans>}
{isBook(mediaItem) && <Trans>Add to read history</Trans>}
{isVideoGame(mediaItem) && <Trans>Add to played history</Trans>}
{isMovie(mediaItem) && <Trans>Add to seen history</Trans>}
{isTvShow(mediaItem) &&
(useSeasonAndEpisodeNumber ? (
episode ? (
<Trans>
Add {formatEpisodeNumber(episode)} to seen history
</Trans>
) : season ? (
<Trans>
Add {formatSeasonNumber(season)} to seen history
</Trans>
) : (
<Trans>Add to seen history</Trans>
)
) : episode ? (
<Trans>Add episode to seen history</Trans>
) : season ? (
<Trans>Add season to seen history</Trans>
) : (
<Trans>Add to seen history</Trans>
))}
</div>
)}
>
{(closeModal) => (
<SelectSeenDate
mediaItem={mediaItem}
season={season}
episode={episode}
closeModal={closeModal}
/>
)}
</Modal>
</>
);
};

export const RemoveFromSeenHistoryButton: FunctionComponent<{
mediaItem: MediaItemDetailsResponse;
season?: TvSeason;
episode?: TvEpisode;
seenId?: number;
}> = (props) => {
const { mediaItem, season, episode, seenId } = props;

const seasonEpisodesIdSet = new Set(
season?.episodes?.map((episode) => episode.id)
);

const count =
seenId !== undefined
? 1
: episode
? mediaItem.seenHistory?.filter(
(entry) => entry.episodeId === episode?.id
).length
: season
? mediaItem.seenHistory?.filter((entry) =>
seasonEpisodesIdSet.has(entry.episodeId)
).length
: mediaItem.seenHistory?.length;

return (
<div
className="text-sm btn-red"
onClick={async () =>
(await Confirm(
plural(count, {
one: 'Do you want to remove # seen history entry?',
other: 'Do you want to remove all # seen history entries?',
})
)) &&
markAsUnseen({
mediaItem: mediaItem,
season: season,
episode: episode,
seenId: seenId,
})
}
>
{isAudiobook(mediaItem) && <Trans>Remove from listened history</Trans>}
{isBook(mediaItem) && <Trans>Remove from read history</Trans>}
{isVideoGame(mediaItem) && <Trans>Remove from played history</Trans>}
{isMovie(mediaItem) && <Trans>Remove from seen history</Trans>}
{isTvShow(mediaItem) &&
(episode ? (
<Trans>Remove episode from seen history</Trans>
) : season ? (
<Trans>Remove season from seen history</Trans>
) : (
<Trans>Remove from seen history</Trans>
))}
</div>
);
};
import React, { FunctionComponent } from 'react';
import { plural, Trans } from '@lingui/macro';

import {
MediaItemDetailsResponse,
MediaItemItemsResponse,
TvEpisode,
TvSeason,
} from 'mediatracker-api';
import {
formatEpisodeNumber,
formatSeasonNumber,
isAudiobook,
isBook,
isMovie,
isMusic,
isTvShow,
isVideoGame,
} from 'src/utils';
import { Modal } from 'src/components/Modal';
import { SelectSeenDate } from 'src/components/SelectSeenDate';
import { markAsUnseen } from 'src/api/details';
import { Confirm } from 'src/components/Confirm';

export const AddToSeenHistoryButton: FunctionComponent<{
mediaItem: MediaItemItemsResponse;
season?: TvSeason;
episode?: TvEpisode;
useSeasonAndEpisodeNumber?: boolean;
}> = (props) => {
const { mediaItem, season, episode, useSeasonAndEpisodeNumber } = {
useSeasonAndEpisodeNumber: false,
...props,
};

return (
<>
<Modal
openModal={(openModal) => (
<div className="text-sm btn-blue bg" onClick={openModal}>
{isAudiobook(mediaItem) && <Trans>Add to listened history</Trans>}
{isBook(mediaItem) && <Trans>Add to read history</Trans>}
{isMusic(mediaItem) && <Trans>Add to listened history</Trans>}
{isVideoGame(mediaItem) && <Trans>Add to played history</Trans>}
{isMovie(mediaItem) && <Trans>Add to seen history</Trans>}
{isTvShow(mediaItem) &&
(useSeasonAndEpisodeNumber ? (
episode ? (
<Trans>
Add {formatEpisodeNumber(episode)} to seen history
</Trans>
) : season ? (
<Trans>
Add {formatSeasonNumber(season)} to seen history
</Trans>
) : (
<Trans>Add to seen history</Trans>
)
) : episode ? (
<Trans>Add episode to seen history</Trans>
) : season ? (
<Trans>Add season to seen history</Trans>
) : (
<Trans>Add to seen history</Trans>
))}
</div>
)}
>
{(closeModal) => (
<SelectSeenDate
mediaItem={mediaItem}
season={season}
episode={episode}
closeModal={closeModal}
/>
)}
</Modal>
</>
);
};

export const RemoveFromSeenHistoryButton: FunctionComponent<{
mediaItem: MediaItemDetailsResponse;
season?: TvSeason;
episode?: TvEpisode;
seenId?: number;
}> = (props) => {
const { mediaItem, season, episode, seenId } = props;

const seasonEpisodesIdSet = new Set(
season?.episodes?.map((episode) => episode.id)
);

const count =
seenId !== undefined
? 1
: episode
? mediaItem.seenHistory?.filter(
(entry) => entry.episodeId === episode?.id
).length
: season
? mediaItem.seenHistory?.filter((entry) =>
seasonEpisodesIdSet.has(entry.episodeId)
).length
: mediaItem.seenHistory?.length;

return (
<div
className="text-sm btn-red"
onClick={async () =>
(await Confirm(
plural(count, {
one: 'Do you want to remove # seen history entry?',
other: 'Do you want to remove all # seen history entries?',
})
)) &&
markAsUnseen({
mediaItem: mediaItem,
season: season,
episode: episode,
seenId: seenId,
})
}
>
{isAudiobook(mediaItem) && <Trans>Remove from listened history</Trans>}
{isBook(mediaItem) && <Trans>Remove from read history</Trans>}
{isMusic(mediaItem) && <Trans>Remove from listened history</Trans>}
{isVideoGame(mediaItem) && <Trans>Remove from played history</Trans>}
{isMovie(mediaItem) && <Trans>Remove from seen history</Trans>}
{isTvShow(mediaItem) &&
(episode ? (
<Trans>Remove episode from seen history</Trans>
) : season ? (
<Trans>Remove season from seen history</Trans>
) : (
<Trans>Remove from seen history</Trans>
))}
</div>
);
};
4 changes: 2 additions & 2 deletions client/src/components/FilterBy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { t } from '@lingui/macro';

import { MediaType } from 'mediatracker-api';
import { isAudiobook, isBook, isVideoGame, reverseMap } from 'src/utils';
import { isAudiobook, isBook, isMusic, isVideoGame, reverseMap } from 'src/utils';
import { useMenuComponent } from 'src/hooks/menu';

const useFilterTextMap = (mediaType: MediaType) => {
Expand All @@ -11,7 +11,7 @@ const useFilterTextMap = (mediaType: MediaType) => {
onlyWithUserRating: t`Rated`,
onlyWithoutUserRating: t`Unrated`,
onlyOnWatchlist: t`On watchlist`,
onlySeenItems: isAudiobook(mediaType)
onlySeenItems: isAudiobook(mediaType) || isMusic(mediaType)
? t`Listened`
: isBook(mediaType)
? t`Read`
Expand Down
1 change: 1 addition & 0 deletions client/src/components/GridItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const GridItem: FunctionComponent<{
movie: t`Movie`,
tv: t`Tv`,
video_game: t`Video game`,
music: t`Music`,
};

if (season && episode) {
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const useRouteNames = () => {
{ path: '/games', name: t`Games` },
{ path: '/books', name: t`Books` },
{ path: '/audiobooks', name: t`Audiobooks` },
{ path: '/music', name: t`Music` },
{ path: '/upcoming', name: t`Upcoming` },
{ path: '/in-progress', name: t`In progress` },
{ path: '/watchlist', name: t`Watchlist` },
Expand Down
14 changes: 1 addition & 13 deletions client/src/components/Poster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ const PosterSpring: FunctionComponent<{
};

const tailwindcssAspectRatioForMediaType = (mediaType?: MediaType) => {
if (mediaType === 'audiobook') {
if (mediaType === 'audiobook' || mediaType === 'music') {
return 'aspect-[1/1]';
}

Expand All @@ -220,16 +220,4 @@ const tailwindcssAspectRatioForMediaType = (mediaType?: MediaType) => {
return 'aspect-[2/3]';
};

const aspectRatioForMediaType = (mediaType?: MediaType) => {
if (mediaType === 'audiobook') {
return 1 / 1;
}

if (mediaType === 'video_game') {
return 3 / 4;
}

return 2 / 3;
};

export { PosterSpring as Poster };
5 changes: 5 additions & 0 deletions client/src/components/SelectSeenDate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
isAudiobook,
isBook,
isMovie,
isMusic,
isTvShow,
isVideoGame,
} from 'src/utils';
Expand Down Expand Up @@ -81,6 +82,10 @@ export const SelectSeenDateComponent: FunctionComponent<{
<Trans>When did you read &quot;{mediaItem.title}&quot;?</Trans>
)}

{isMusic(mediaItem) && (
<Trans>When did you listen to &quot;{mediaItem.title}&quot;?</Trans>
)}

{isMovie(mediaItem) && (
<Trans>When did you see &quot;{mediaItem.title}&quot;?</Trans>
)}
Expand Down
Loading