parent
aceaaa10e1
commit
311cd66fcd
|
@ -8,6 +8,7 @@ import Icon from 'Components/Icon';
|
|||
import Link from 'Components/Link/Link';
|
||||
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import getFinaleTypeName from 'Episode/getFinaleTypeName';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
|
@ -52,6 +53,7 @@ class AgendaEvent extends Component {
|
|||
airDateUtc,
|
||||
monitored,
|
||||
unverifiedSceneNumbering,
|
||||
finaleType,
|
||||
hasFile,
|
||||
grabbed,
|
||||
queueItem,
|
||||
|
@ -71,8 +73,6 @@ class AgendaEvent extends Component {
|
|||
const isMonitored = series.monitored && monitored;
|
||||
const statusStyle = getStatusStyle(hasFile, downloading, startTime, endTime, isMonitored);
|
||||
const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
|
||||
const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
|
||||
const seasonStatistics = season?.statistics || {};
|
||||
|
||||
return (
|
||||
<div className={styles.event}>
|
||||
|
@ -189,15 +189,14 @@ class AgendaEvent extends Component {
|
|||
|
||||
{
|
||||
showFinaleIcon &&
|
||||
episodeNumber !== 1 &&
|
||||
seasonNumber > 0 &&
|
||||
episodeNumber === seasonStatistics.totalEpisodeCount &&
|
||||
finaleType ?
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={kinds.WARNING}
|
||||
title={series.status === 'ended' ? translate('SeriesFinale') : translate('SeasonFinale')}
|
||||
/>
|
||||
title={getFinaleTypeName(finaleType)}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -238,6 +237,7 @@ AgendaEvent.propTypes = {
|
|||
airDateUtc: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
unverifiedSceneNumbering: PropTypes.bool,
|
||||
finaleType: PropTypes.string,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
grabbed: PropTypes.bool,
|
||||
queueItem: PropTypes.object,
|
||||
|
|
|
@ -7,6 +7,7 @@ import Icon from 'Components/Icon';
|
|||
import Link from 'Components/Link/Link';
|
||||
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import getFinaleTypeName from 'Episode/getFinaleTypeName';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
|
@ -57,6 +58,7 @@ class CalendarEvent extends Component {
|
|||
airDateUtc,
|
||||
monitored,
|
||||
unverifiedSceneNumbering,
|
||||
finaleType,
|
||||
hasFile,
|
||||
grabbed,
|
||||
queueItem,
|
||||
|
@ -79,8 +81,6 @@ class CalendarEvent extends Component {
|
|||
const isMonitored = series.monitored && monitored;
|
||||
const statusStyle = getStatusStyle(hasFile, isDownloading, startTime, endTime, isMonitored);
|
||||
const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
|
||||
const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
|
||||
const seasonStatistics = season?.statistics || {};
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -170,14 +170,12 @@ class CalendarEvent extends Component {
|
|||
|
||||
{
|
||||
showFinaleIcon &&
|
||||
episodeNumber !== 1 &&
|
||||
seasonNumber > 0 &&
|
||||
episodeNumber === seasonStatistics.totalEpisodeCount ?
|
||||
finaleType ?
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
title={series.status === 'ended' ? translate('SeriesFinale') : translate('SeasonFinale')}
|
||||
title={getFinaleTypeName(finaleType)}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
@ -247,6 +245,7 @@ CalendarEvent.propTypes = {
|
|||
airDateUtc: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
unverifiedSceneNumbering: PropTypes.bool,
|
||||
finaleType: PropTypes.string,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
grabbed: PropTypes.bool,
|
||||
queueItem: PropTypes.object,
|
||||
|
|
|
@ -6,6 +6,7 @@ import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
|
|||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import getFinaleTypeName from 'Episode/getFinaleTypeName';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
|
@ -175,15 +176,13 @@ class CalendarEventGroup extends Component {
|
|||
|
||||
{
|
||||
showFinaleIcon &&
|
||||
lastEpisode.episodeNumber !== 1 &&
|
||||
seasonNumber > 0 &&
|
||||
lastEpisode.episodeNumber === series.seasons.find((season) => season.seasonNumber === seasonNumber).statistics.totalEpisodeCount &&
|
||||
lastEpisode.finaleType ?
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
title={series.status === 'ended' ? translate('SeriesFinale') : translate('SeasonFinale')}
|
||||
/>
|
||||
title={getFinaleTypeName(lastEpisode.finaleType)}
|
||||
/> : null
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'container': string;
|
||||
'link': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
||||
import styles from './EpisodeTitleLink.css';
|
||||
|
||||
class EpisodeTitleLink extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onLinkPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
episodeTitle,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
className={styles.link}
|
||||
onPress={this.onLinkPress}
|
||||
>
|
||||
{episodeTitle}
|
||||
</Link>
|
||||
|
||||
<EpisodeDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
episodeTitle={episodeTitle}
|
||||
{...otherProps}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeTitleLink.propTypes = {
|
||||
episodeTitle: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
EpisodeTitleLink.defaultProps = {
|
||||
showSeriesButton: false
|
||||
};
|
||||
|
||||
export default EpisodeTitleLink;
|
|
@ -0,0 +1,46 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
||||
import FinaleType from './FinaleType';
|
||||
import styles from './EpisodeTitleLink.css';
|
||||
|
||||
interface EpisodeTitleLinkProps {
|
||||
episodeTitle: string;
|
||||
finaleType?: string;
|
||||
}
|
||||
|
||||
function EpisodeTitleLink(props: EpisodeTitleLinkProps) {
|
||||
const { episodeTitle, finaleType, ...otherProps } = props;
|
||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||
const handleLinkPress = useCallback(() => {
|
||||
setIsDetailsModalOpen(true);
|
||||
}, [setIsDetailsModalOpen]);
|
||||
const handleModalClose = useCallback(() => {
|
||||
setIsDetailsModalOpen(false);
|
||||
}, [setIsDetailsModalOpen]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Link className={styles.link} onPress={handleLinkPress}>
|
||||
{episodeTitle}
|
||||
</Link>
|
||||
|
||||
{finaleType ? <FinaleType finaleType={finaleType} /> : null}
|
||||
|
||||
<EpisodeDetailsModal
|
||||
isOpen={isDetailsModalOpen}
|
||||
episodeTitle={episodeTitle}
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EpisodeTitleLink.propTypes = {
|
||||
episodeTitle: PropTypes.string.isRequired,
|
||||
finaleType: PropTypes.string,
|
||||
};
|
||||
|
||||
export default EpisodeTitleLink;
|
|
@ -0,0 +1,5 @@
|
|||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin-left: 10px;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'label': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,29 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import getFinaleTypeName from './getFinaleTypeName';
|
||||
import styles from './FinaleType.css';
|
||||
|
||||
interface SeriesStatusCellProps {
|
||||
finaleType: string;
|
||||
}
|
||||
|
||||
function FinaleType(props: SeriesStatusCellProps) {
|
||||
const { finaleType } = props;
|
||||
|
||||
const finaleText = useMemo(() => {
|
||||
return getFinaleTypeName(finaleType);
|
||||
}, [finaleType]);
|
||||
|
||||
if (finaleType == null || finaleText == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label className={styles.label} kind={kinds.INFO}>
|
||||
{finaleText}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
export default FinaleType;
|
|
@ -0,0 +1,14 @@
|
|||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export default function getFinaleTypeName(finaleType?: string): string | null {
|
||||
switch (finaleType) {
|
||||
case 'series':
|
||||
return translate('SeriesFinale');
|
||||
case 'season':
|
||||
return translate('SeasonFinale');
|
||||
case 'midseason':
|
||||
return translate('MidseasonFinale');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -64,6 +64,7 @@ class EpisodeRow extends Component {
|
|||
sceneAbsoluteEpisodeNumber,
|
||||
airDateUtc,
|
||||
runtime,
|
||||
finaleType,
|
||||
title,
|
||||
useSceneNumbering,
|
||||
unverifiedSceneNumbering,
|
||||
|
@ -141,6 +142,7 @@ class EpisodeRow extends Component {
|
|||
episodeId={id}
|
||||
seriesId={seriesId}
|
||||
episodeTitle={title}
|
||||
finaleType={finaleType}
|
||||
showOpenSeriesButton={false}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
@ -366,6 +368,7 @@ EpisodeRow.propTypes = {
|
|||
sceneAbsoluteEpisodeNumber: PropTypes.number,
|
||||
airDateUtc: PropTypes.string,
|
||||
runtime: PropTypes.number,
|
||||
finaleType: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
useSceneNumbering: PropTypes.bool,
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(196)]
|
||||
public class add_finale_type : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Episodes").AddColumn("FinaleType").AsString().Nullable();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -693,6 +693,7 @@
|
|||
"MetadataSource": "Metadata Source",
|
||||
"MetadataSourceSettings": "Metadata Source Settings",
|
||||
"MetadataSourceSettingsSummary": "Information on where Sonarr gets series and episode information",
|
||||
"MidseasonFinale": "Midseason Finale",
|
||||
"Min": "Min",
|
||||
"MinimumAge": "Minimum Age",
|
||||
"MinimumAgeHelpText": "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider.",
|
||||
|
|
|
@ -15,6 +15,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
|
|||
public string AirDate { get; set; }
|
||||
public DateTime? AirDateUtc { get; set; }
|
||||
public int Runtime { get; set; }
|
||||
public string FinaleType { get; set; }
|
||||
public RatingResource Rating { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public string Image { get; set; }
|
||||
|
|
|
@ -261,6 +261,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
episode.AirDate = oracleEpisode.AirDate;
|
||||
episode.AirDateUtc = oracleEpisode.AirDateUtc;
|
||||
episode.Runtime = oracleEpisode.Runtime;
|
||||
episode.FinaleType = oracleEpisode.FinaleType;
|
||||
|
||||
episode.Ratings = MapRatings(oracleEpisode.Rating);
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ namespace NzbDrone.Core.Tv
|
|||
public List<MediaCover.MediaCover> Images { get; set; }
|
||||
public DateTime? LastSearchTime { get; set; }
|
||||
public int Runtime { get; set; }
|
||||
public string FinaleType { get; set; }
|
||||
|
||||
public string SeriesTitle { get; private set; }
|
||||
|
||||
|
|
|
@ -45,7 +45,10 @@ namespace NzbDrone.Core.Tv
|
|||
dupeFreeRemoteEpisodes = MapAbsoluteEpisodeNumbers(dupeFreeRemoteEpisodes);
|
||||
}
|
||||
|
||||
foreach (var episode in OrderEpisodes(series, dupeFreeRemoteEpisodes))
|
||||
var orderedEpisodes = OrderEpisodes(series, dupeFreeRemoteEpisodes).ToList();
|
||||
var episodesPerSeason = orderedEpisodes.GroupBy(s => s.SeasonNumber).ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
foreach (var episode in orderedEpisodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -76,9 +79,16 @@ namespace NzbDrone.Core.Tv
|
|||
episodeToUpdate.AirDate = episode.AirDate;
|
||||
episodeToUpdate.AirDateUtc = episode.AirDateUtc;
|
||||
episodeToUpdate.Runtime = episode.Runtime;
|
||||
episodeToUpdate.FinaleType = episode.FinaleType;
|
||||
episodeToUpdate.Ratings = episode.Ratings;
|
||||
episodeToUpdate.Images = episode.Images;
|
||||
|
||||
// TheTVDB has a severe lack of season/series finales, this helps smooth out that limitation so they can be displayed in the UI
|
||||
if (episodeToUpdate.FinaleType == null && episodeToUpdate.SeasonNumber > 0 && episodeToUpdate.EpisodeNumber > 1 && episodeToUpdate.EpisodeNumber == episodesPerSeason[episodeToUpdate.SeasonNumber])
|
||||
{
|
||||
episodeToUpdate.FinaleType = series.Status == SeriesStatusType.Ended ? "series" : "season";
|
||||
}
|
||||
|
||||
successCount++;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace Sonarr.Api.V3.Episodes
|
|||
public string AirDate { get; set; }
|
||||
public DateTime? AirDateUtc { get; set; }
|
||||
public int Runtime { get; set; }
|
||||
public string FinaleType { get; set; }
|
||||
public string Overview { get; set; }
|
||||
public EpisodeFileResource EpisodeFile { get; set; }
|
||||
public bool HasFile { get; set; }
|
||||
|
@ -64,6 +65,7 @@ namespace Sonarr.Api.V3.Episodes
|
|||
AirDate = model.AirDate,
|
||||
AirDateUtc = model.AirDateUtc,
|
||||
Runtime = model.Runtime,
|
||||
FinaleType = model.FinaleType,
|
||||
Overview = model.Overview,
|
||||
|
||||
// EpisodeFile
|
||||
|
|
Loading…
Reference in New Issue