Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/replace season pack #172

Merged
merged 8 commits into from
Dec 20, 2020
Merged
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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### Added

- manual search season pack (/~https://github.com/iam4x/bobarr/pull/172)

### Added

- added FlareSolverr for solving CloudFare on certains trackers within jackett (/~https://github.com/iam4x/bobarr/issues/165)
- update jobs ui
- update nodejs to v14
Expand Down
1 change: 1 addition & 0 deletions packages/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ type Mutation {
startFindNewEpisodesJob: GraphQLCommonResponse!
startDownloadMissingJob: GraphQLCommonResponse!
downloadMovie(jackettResult: JackettInput!, movieId: Int!): GraphQLCommonResponse!
downloadSeason(jackettResult: JackettInput!, seasonNumber: Int!, tvShowTMDBId: Int!): GraphQLCommonResponse!
downloadTVEpisode(jackettResult: JackettInput!, episodeId: Int!): GraphQLCommonResponse!
trackMovie(tmdbId: Int!, title: String!): Movie!
removeMovie(tmdbId: Int!): GraphQLCommonResponse!
Expand Down
30 changes: 29 additions & 1 deletion packages/api/src/modules/library/library.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ import {
} from './library.dto';

import { makeCacheInterceptor } from '../redis/cache.interceptor';
import { TVShowDAO } from 'src/entities/dao/tvshow.dao';

@Resolver()
export class LibraryResolver {
public constructor(private readonly libraryService: LibraryService) {}
public constructor(
private readonly libraryService: LibraryService,
private readonly tvShowDAO: TVShowDAO
) {}

@Query((_returns) => [DownloadingMedia])
public getDownloadingMedias() {
Expand Down Expand Up @@ -97,6 +101,30 @@ export class LibraryResolver {
return { success: true, message: 'MOVIE_DOWNLOAD_STARTED' };
}

@Mutation((_returns) => GraphQLCommonResponse)
public async downloadSeason(
@Args('tvShowTMDBId', { type: () => Int }) tvShowTMDBId: number,
@Args('seasonNumber', { type: () => Int }) seasonNumber: number,
@Args('jackettResult', { type: () => JackettInput })
jackettResult: JackettInput
) {
const { seasons } = await this.tvShowDAO
.createQueryBuilder('tvShow')
.innerJoinAndSelect(
'tvShow.seasons',
'season',
'season.seasonNumber = :seasonNumber',
{ seasonNumber }
)
.where('tvShow.tmdbId = :tvShowTMDBId', { tvShowTMDBId })
.getOneOrFail();

const [{ id: seasonId }] = seasons;
await this.libraryService.downloadTVSeason(seasonId, jackettResult, null);

return { success: true, message: 'TV_EPISODE_DOWNLOAD_STARTED' };
}

@Mutation((_returns) => GraphQLCommonResponse)
public async downloadTVEpisode(
@Args('episodeId', { type: () => Int }) episodeId: number,
Expand Down
68 changes: 64 additions & 4 deletions packages/api/src/modules/library/library.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,7 @@ export class LibraryService {
this.logger.info('start download tv season', { seasonId });
this.logger.info(jackettResult.title);

await manager!.getCustomRepository(TVSeasonDAO).save({
id: seasonId,
state: DownloadableMediaState.DOWNLOADING,
});
await this.replaceSeason(seasonId, manager!);

const torrent = await this.transmissionService.addTorrent(
{
Expand Down Expand Up @@ -580,6 +577,10 @@ export class LibraryService {
},
} as const;

if (mediaType === FileType.SEASON) {
await this.replaceSeason(mediaId, manager!);
}

if (mediaType === FileType.EPISODE) {
await this.replaceTVEpisode(mediaId, manager!);
}
Expand All @@ -600,6 +601,65 @@ export class LibraryService {
});
}

private async replaceSeason(seasonId: number, manager: EntityManager) {
const tvSeasonDAO = manager!.getCustomRepository(TVSeasonDAO);
const torrentDAO = manager!.getCustomRepository(TorrentDAO);
const tvEpisodeDAO = manager!.getCustomRepository(TVEpisodeDAO);
const fileDAO = manager!.getCustomRepository(FileDAO);

const tvSeason = await tvSeasonDAO.findOneOrFail({
where: { id: seasonId },
relations: ['episodes', 'episodes.files'],
});

if (tvSeason.state !== DownloadableMediaState.MISSING) {
this.logger.info('tv season already download, removing existing files');

await forEach(tvSeason.episodes, async (episode) => {
const torrent = await torrentDAO.findOne({
resourceId: episode.id,
resourceType: FileType.EPISODE,
});

if (torrent) {
await torrentDAO.remove(torrent);
await this.transmissionService.removeTorrentAndFiles(
torrent.torrentHash
);
this.logger.info('episode torrent removed', { torrent: torrent.id });
}
});

const tvSeasonFolders = uniq(
flatten(
tvSeason.episodes.map((episode) =>
episode.files.map((file) => path.dirname(file.path))
)
)
);

await forEachSeries(tvSeasonFolders, (folder) =>
childCommand(`rm -rf "${folder}"`)
);

await fileDAO.remove(
flatten(tvSeason.episodes.map((episode) => episode.files))
);
}

await tvEpisodeDAO.save(
tvSeason.episodes.map((v) => ({
id: v.id,
state: DownloadableMediaState.SEARCHING,
}))
);

await tvSeasonDAO.save({
id: seasonId,
state: DownloadableMediaState.DOWNLOADING,
});
}

private async replaceTVEpisode(episodeId: number, manager: EntityManager) {
const tvEpisodeDAO = manager!.getCustomRepository(TVEpisodeDAO);
const torrentDAO = manager!.getCustomRepository(TorrentDAO);
Expand Down
33 changes: 32 additions & 1 deletion packages/web/components/manual-search/jackett-results-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
GetMissingDocument,
useDownloadTvEpisodeMutation,
GetLibraryTvShowsDocument,
useDownloadSeasonMutation,
} from '../../utils/graphql';

import { Media } from './manual-search.helpers';
Expand Down Expand Up @@ -155,6 +156,26 @@ function ManualDownloadMedia({
}),
});

const [downloadTVSeason, { loading: loading3 }] = useDownloadSeasonMutation({
awaitRefetchQueries: true,
refetchQueries: [
{ query: GetLibraryTvShowsDocument },
{ query: GetDownloadingDocument },
{ query: GetMissingDocument },
...refetchQueries,
],
onError: ({ message }) =>
notification.error({
message: message.replace('GraphQL error: ', ''),
placement: 'bottomRight',
}),
onCompleted: () =>
notification.success({
message: 'Download episode started',
placement: 'bottomRight',
}),
});

const handleClick = () => {
if (media.__typename === 'EnrichedMovie') {
downloadMovie({
Expand All @@ -173,9 +194,19 @@ function ManualDownloadMedia({
},
});
}

if (media.__typename === 'TMDBFormattedTVSeason') {
downloadTVSeason({
variables: {
tvShowTMDBId: media.tvShowTMDBId!,
seasonNumber: media.seasonNumber!,
jackettResult: jackettInput,
},
});
}
};

return loading1 || loading2 ? (
return loading1 || loading2 || loading3 ? (
<LoadingOutlined />
) : (
<DownloadOutlined style={{ cursor: 'pointer' }} onClick={handleClick} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
MissingMoviesFragment,
EnrichedMovie,
EnrichedTvEpisode,
TmdbFormattedTvSeason,
} from '../../utils/graphql';

export type Media =
| MissingTvEpisodesFragment
| MissingMoviesFragment
| EnrichedMovie
| EnrichedTvEpisode;
| EnrichedTvEpisode
| (TmdbFormattedTvSeason & { tvShowTitle: string; tvShowTMDBId: number });

export function getDefaultSearchQuery(media: Media) {
if (media.__typename === 'EnrichedTVEpisode') {
Expand All @@ -27,5 +29,10 @@ export function getDefaultSearchQuery(media: Media) {
return `${media.title} ${year}`;
}

if (media.__typename === 'TMDBFormattedTVSeason') {
const seasonNb = formatNumber(media.seasonNumber!);
return `${media.tvShowTitle} S${seasonNb}`;
}

return '';
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@ import {

import { availableIn } from '../../utils/available-in';
import { ManualSearchComponent } from '../manual-search/manual-search.component';
import { Media } from '../manual-search/manual-search.helpers';

interface TVSeasonDetailsProps {
tvShowTMDBId: number;
season: TmdbFormattedTvSeason;
tvShowTitle: string;
}

export function TVSeasonDetailsComponent({
tvShowTMDBId,
season,
tvShowTitle,
}: TVSeasonDetailsProps) {
const [isOpen, setIsOpen] = useState(false);
const [manualSearch, setManualSearch] = useState<EnrichedTvEpisode | null>(
null
);
const [manualSearch, setManualSearch] = useState<Media | null>(null);

const { data, loading } = useGetTvSeasonDetailsQuery({
pollInterval: 5000,
Expand Down Expand Up @@ -90,9 +91,9 @@ export function TVSeasonDetailsComponent({
<Tag
icon={<SearchOutlined />}
onClick={() => setManualSearch(row)}
style={{ width: 80, textAlign: 'center', cursor: 'pointer' }}
style={{ width: 120, textAlign: 'center', cursor: 'pointer' }}
>
{inLibrary ? 'Replace' : 'Search'}
{inLibrary ? 'Replace' : 'Search'} episode
</Tag>
);
},
Expand Down Expand Up @@ -121,17 +122,28 @@ export function TVSeasonDetailsComponent({
className="season"
style={{ marginBottom: isOpen && season.seasonNumber !== 1 ? 12 : 0 }}
>
<div className="season-title" onClick={toggle}>
<div className="season-toggle">
{isOpen ? <FaChevronCircleDown /> : <FaChevronCircleRight />}
</div>
<div className="season-number">Season {season.seasonNumber}</div>
{season.airDate && (
<div className="season-year">
{' '}
({dayjs(season.airDate).format('YYYY')})
<div className="season-top">
<div className="season-title" onClick={toggle}>
<div className="season-toggle">
{isOpen ? <FaChevronCircleDown /> : <FaChevronCircleRight />}
</div>
)}
<div className="season-number">Season {season.seasonNumber}</div>
{season.airDate && (
<div className="season-year">
{' '}
({dayjs(season.airDate).format('YYYY')})
</div>
)}
</div>
<div
className="season-replace"
onClick={() =>
setManualSearch({ ...season, tvShowTitle, tvShowTMDBId })
}
>
{season.inLibrary ? 'Replace' : 'Search'} season
<SearchOutlined style={{ marginLeft: 8 }} />
</div>
</div>
{isOpen && (
<Table<EnrichedTvEpisode>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export function TVShowSeasonsModalComponent(
key={season.id}
season={season}
tvShowTMDBId={tvShow.tmdbId}
tvShowTitle={tvShow.title}
/>
))}
</div>
Expand Down
47 changes: 33 additions & 14 deletions packages/web/components/tvshow-details/tvshow-details.styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,45 @@ export const TVShowSeasonsModalComponentStyles = styled(MovieDetailsStyles)`
.seasons-details {
padding-top: 12px;

.season-title {
.season-top {
margin-bottom: 4px;
}

.season-title,
.season-top {
display: flex;
align-items: center;
}

.season-title,
.season-replace {
cursor: pointer;
}

.season-number {
font-size: 1.25em;
font-weight: 600;
margin-right: 8px;
}
.season-replace {
font-weight: bold;
display: flex;
align-items: center;
margin-left: 32px;
border: 1px solid #fff5;
border-radius: 5px;
padding: 0 4px;
}

.season-year {
font-size: 1em;
font-weight: 300;
}
.season-number {
font-size: 1.25em;
font-weight: 600;
margin-right: 8px;
}

.season-toggle {
margin-right: 12px;
margin-top: 4px;
}
.season-year {
font-size: 1em;
font-weight: 300;
}

.season-toggle {
margin-right: 12px;
margin-top: 4px;
}

.ant-table {
Expand Down
Loading