mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 08:28:37 +01:00
New: Show grabbed/blocklisted releases in Interactive Search
Closes #3955
This commit is contained in:
parent
10964b625e
commit
e8c63405d2
13 changed files with 270 additions and 71 deletions
|
|
@ -26,6 +26,7 @@ import {
|
|||
faArrowCircleRight as fasArrowCircleRight,
|
||||
faAsterisk as fasAsterisk,
|
||||
faBackward as fasBackward,
|
||||
faBan as fasBan,
|
||||
faBars as fasBars,
|
||||
faBolt as fasBolt,
|
||||
faBookmark as fasBookmark,
|
||||
|
|
@ -124,6 +125,7 @@ export const ADVANCED_SETTINGS = fasCog;
|
|||
export const ARROW_LEFT = fasArrowCircleLeft;
|
||||
export const ARROW_RIGHT = fasArrowCircleRight;
|
||||
export const BACKUP = farFileArchive;
|
||||
export const BLOCKLIST = fasBan;
|
||||
export const BUG = fasBug;
|
||||
export const CALENDAR = fasCalendarAlt;
|
||||
export const CALENDAR_O = farCalendar;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,16 @@
|
|||
width: 50px;
|
||||
}
|
||||
|
||||
.history {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.blocklistIconContainer {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.age,
|
||||
.size {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'age': string;
|
||||
'blocklistIconContainer': string;
|
||||
'customFormatScore': string;
|
||||
'download': string;
|
||||
'downloadIcon': string;
|
||||
'history': string;
|
||||
'indexer': string;
|
||||
'indexerFlags': string;
|
||||
'interactiveIcon': string;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
|
|
@ -78,6 +78,7 @@ interface InteractiveSearchRowProps extends Release {
|
|||
function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
const {
|
||||
decision,
|
||||
history,
|
||||
parsedInfo,
|
||||
release,
|
||||
publishDate,
|
||||
|
|
@ -129,6 +130,12 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||
const { isGrabbing, isGrabbed, grabError, grabRelease } = useGrabRelease();
|
||||
|
||||
const isBlocklisted = useMemo(() => {
|
||||
return (
|
||||
decision.rejections.findIndex((r) => r.reason === 'blocklisted') >= 0
|
||||
);
|
||||
}, [decision]);
|
||||
|
||||
const handleGrabPress = useCallback(() => {
|
||||
if (downloadAllowed) {
|
||||
grabRelease({
|
||||
|
|
@ -206,6 +213,56 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||
|
||||
<TableRowCell className={styles.indexer}>{indexer}</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.history}>
|
||||
{history ? (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={history.failed ? kinds.DANGER : kinds.DEFAULT}
|
||||
title={`${
|
||||
history.failed
|
||||
? translate('FailedAt', {
|
||||
date: formatDateTime(
|
||||
history.failed,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
),
|
||||
})
|
||||
: translate('GrabbedAt', {
|
||||
date: formatDateTime(
|
||||
history.grabbed,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
),
|
||||
})
|
||||
}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isBlocklisted ? (
|
||||
<Icon
|
||||
containerClassName={
|
||||
history ? styles.blocklistIconContainer : undefined
|
||||
}
|
||||
name={icons.BLOCKLIST}
|
||||
kind={kinds.DANGER}
|
||||
title={
|
||||
history?.failed
|
||||
? `${translate('BlockListedAt', {
|
||||
date: formatDateTime(
|
||||
history.failed,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
),
|
||||
})}`
|
||||
: translate('Blocklisted')
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.size}>{formatBytes(size)}</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.peers}>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,13 @@ const { useOptions, useOption, getOptions, getOption, setOptions, setOption } =
|
|||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
label: translate('History'),
|
||||
isSortable: true,
|
||||
fixedSortDirection: 'ascending',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export interface Release extends ModelBase {
|
|||
parsedInfo: ParsedInfo;
|
||||
release: ReleaseInfo;
|
||||
decision: Decision;
|
||||
history?: ReleaseHistory;
|
||||
qualityWeight: number;
|
||||
languages: Language[];
|
||||
mappedSeriesId?: number;
|
||||
|
|
@ -104,6 +105,11 @@ export interface Decision {
|
|||
rejections: Rejection[];
|
||||
}
|
||||
|
||||
export interface ReleaseHistory {
|
||||
grabbed: string;
|
||||
failed: string;
|
||||
}
|
||||
|
||||
export const FILTERS: Filter[] = [
|
||||
{
|
||||
key: 'all',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
|
||||
|
|
@ -109,62 +110,12 @@ public void Delete(List<int> ids)
|
|||
|
||||
private bool SameNzb(Blocklist item, ReleaseInfo release)
|
||||
{
|
||||
if (item.PublishedDate == release.PublishDate)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!HasSameIndexer(item, release.Indexer) &&
|
||||
HasSamePublishedDate(item, release.PublishDate) &&
|
||||
HasSameSize(item, release.Size))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return ReleaseComparer.SameNzb(new ReleaseComparerModel(item), release);
|
||||
}
|
||||
|
||||
private bool SameTorrent(Blocklist item, TorrentInfo release)
|
||||
{
|
||||
if (release.InfoHash.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return release.InfoHash.Equals(item.TorrentInfoHash, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
return HasSameIndexer(item, release.Indexer);
|
||||
}
|
||||
|
||||
private bool HasSameIndexer(Blocklist item, string indexer)
|
||||
{
|
||||
if (item.Indexer.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.Indexer.Equals(indexer, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
private bool HasSamePublishedDate(Blocklist item, DateTime publishedDate)
|
||||
{
|
||||
if (!item.PublishedDate.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.PublishedDate.Value.AddMinutes(-2) <= publishedDate &&
|
||||
item.PublishedDate.Value.AddMinutes(2) >= publishedDate;
|
||||
}
|
||||
|
||||
private bool HasSameSize(Blocklist item, long size)
|
||||
{
|
||||
if (!item.Size.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var difference = Math.Abs(item.Size.Value - size);
|
||||
|
||||
return difference <= 2.Megabytes();
|
||||
return ReleaseComparer.SameTorrent(new ReleaseComparerModel(item), release);
|
||||
}
|
||||
|
||||
public void Execute(ClearBlocklistCommand message)
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@
|
|||
"BlackholeWatchFolder": "Watch Folder",
|
||||
"BlackholeWatchFolderHelpText": "Folder from which {appName} should import completed downloads",
|
||||
"Blocklist": "Blocklist",
|
||||
"BlockListedAt": "Blocklisted at {date}",
|
||||
"BlocklistAndSearch": "Blocklist and Search",
|
||||
"BlocklistAndSearchHint": "Start a search for a replacement after blocklisting",
|
||||
"BlocklistAndSearchMultipleHint": "Start searches for replacements after blocklisting",
|
||||
|
|
@ -706,6 +707,7 @@
|
|||
"ExtraFileExtensionsHelpText": "Comma separated list of extra files to import (.nfo will be imported as .nfo-orig)",
|
||||
"ExtraFileExtensionsHelpTextsExamples": "Examples: '.sub, .nfo' or 'sub,nfo'",
|
||||
"Failed": "Failed",
|
||||
"FailedAt": "Failed at: {date}",
|
||||
"FailedToFetchSettings": "Failed to fetch settings",
|
||||
"FailedToFetchUpdates": "Failed to fetch updates",
|
||||
"FailedToLoadCustomFiltersFromApi": "Failed to load custom filters from API",
|
||||
|
|
@ -795,6 +797,7 @@
|
|||
"GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} was unable to determine which series and episode this release was for. {appName} may be unable to automatically import this release. Do you want to grab '{title}'?",
|
||||
"GrabSelected": "Grab Selected",
|
||||
"Grabbed": "Grabbed",
|
||||
"GrabbedAt": "Grabbed at: {date}",
|
||||
"Group": "Group",
|
||||
"HardlinkCopyFiles": "Hardlink/Copy Files",
|
||||
"HasMissingSeason": "Has Missing Season",
|
||||
|
|
|
|||
32
src/NzbDrone.Core/Parser/Model/ReleaseComparerModel.cs
Normal file
32
src/NzbDrone.Core/Parser/Model/ReleaseComparerModel.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.History;
|
||||
|
||||
namespace NzbDrone.Core.Parser.Model;
|
||||
|
||||
public class ReleaseComparerModel
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string TorrentInfoHash { get; set; }
|
||||
public DateTime? PublishedDate { get; set; }
|
||||
public string Indexer { get; set; }
|
||||
public long Size { get; set; }
|
||||
|
||||
public ReleaseComparerModel(Blocklist blocklist)
|
||||
{
|
||||
Title = blocklist.SourceTitle;
|
||||
TorrentInfoHash = blocklist.TorrentInfoHash;
|
||||
PublishedDate = blocklist.PublishedDate;
|
||||
Indexer = blocklist.Indexer;
|
||||
Size = blocklist.Size ?? 0;
|
||||
}
|
||||
|
||||
public ReleaseComparerModel(EpisodeHistory history)
|
||||
{
|
||||
Title = history.SourceTitle;
|
||||
PublishedDate = history.Date;
|
||||
Indexer = history.Data.GetValueOrDefault("indexer");
|
||||
Size = long.Parse(history.Data.GetValueOrDefault("size", "0"));
|
||||
}
|
||||
}
|
||||
68
src/NzbDrone.Core/Parser/ReleaseComparer.cs
Normal file
68
src/NzbDrone.Core/Parser/ReleaseComparer.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Parser;
|
||||
|
||||
public static class ReleaseComparer
|
||||
{
|
||||
public static bool SameNzb(ReleaseComparerModel item, ReleaseInfo release)
|
||||
{
|
||||
if (item.PublishedDate == release.PublishDate)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!HasSameIndexer(item, release.Indexer) &&
|
||||
HasSamePublishedDate(item, release.PublishDate) &&
|
||||
HasSameSize(item, release.Size))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool SameTorrent(ReleaseComparerModel item, TorrentInfo release)
|
||||
{
|
||||
if (release.InfoHash.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return release.InfoHash.Equals(item.TorrentInfoHash, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
return HasSameIndexer(item, release.Indexer);
|
||||
}
|
||||
|
||||
private static bool HasSameIndexer(ReleaseComparerModel item, string indexer)
|
||||
{
|
||||
if (item.Indexer.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.Indexer.Equals(indexer, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool HasSamePublishedDate(ReleaseComparerModel item, DateTime publishedDate)
|
||||
{
|
||||
if (!item.PublishedDate.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.PublishedDate.Value.AddMinutes(-2) <= publishedDate &&
|
||||
item.PublishedDate.Value.AddMinutes(2) >= publishedDate;
|
||||
}
|
||||
|
||||
private static bool HasSameSize(ReleaseComparerModel item, long size)
|
||||
{
|
||||
if (item.Size == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var difference = Math.Abs(item.Size - size);
|
||||
|
||||
return difference <= 2.Megabytes();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
|
@ -31,6 +32,7 @@ public class ReleaseController : RestController<ReleaseResource>
|
|||
private readonly ISeriesService _seriesService;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly QualityProfile _qualityProfile;
|
||||
|
|
@ -44,6 +46,7 @@ public ReleaseController(IFetchAndParseRss rssFetcherAndParser,
|
|||
ISeriesService seriesService,
|
||||
IEpisodeService episodeService,
|
||||
IParsingService parsingService,
|
||||
IHistoryService historyService,
|
||||
ICacheManager cacheManager,
|
||||
IQualityProfileService qualityProfileService,
|
||||
Logger logger)
|
||||
|
|
@ -56,6 +59,7 @@ public ReleaseController(IFetchAndParseRss rssFetcherAndParser,
|
|||
_seriesService = seriesService;
|
||||
_episodeService = episodeService;
|
||||
_parsingService = parsingService;
|
||||
_historyService = historyService;
|
||||
_logger = logger;
|
||||
|
||||
_qualityProfile = qualityProfileService.GetDefaultProfile(string.Empty);
|
||||
|
|
@ -204,8 +208,9 @@ private async Task<List<ReleaseResource>> GetEpisodeReleases(int episodeId)
|
|||
{
|
||||
var decisions = await _releaseSearchService.EpisodeSearch(episodeId, true, true);
|
||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
|
||||
var history = _historyService.FindByEpisodeId(episodeId);
|
||||
|
||||
return MapDecisions(prioritizedDecisions);
|
||||
return MapDecisions(prioritizedDecisions, history);
|
||||
}
|
||||
catch (SearchFailedException ex)
|
||||
{
|
||||
|
|
@ -224,8 +229,9 @@ private async Task<List<ReleaseResource>> GetSeasonReleases(int seriesId, int se
|
|||
{
|
||||
var decisions = await _releaseSearchService.SeasonSearch(seriesId, seasonNumber, false, false, true, true);
|
||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
|
||||
var history = _historyService.GetBySeason(seriesId, seasonNumber, null);
|
||||
|
||||
return MapDecisions(prioritizedDecisions);
|
||||
return MapDecisions(prioritizedDecisions, history);
|
||||
}
|
||||
catch (SearchFailedException ex)
|
||||
{
|
||||
|
|
@ -244,7 +250,7 @@ private async Task<List<ReleaseResource>> GetRss()
|
|||
var decisions = _downloadDecisionMaker.GetRssDecision(reports);
|
||||
var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions);
|
||||
|
||||
return MapDecisions(prioritizedDecisions);
|
||||
return MapDecisions(prioritizedDecisions, new List<EpisodeHistory>());
|
||||
}
|
||||
|
||||
private string GetCacheKey(ReleaseResource resource)
|
||||
|
|
@ -257,7 +263,7 @@ private string GetCacheKey(ReleaseGrabResource resource)
|
|||
return string.Concat(resource.IndexerId, "_", resource.Guid);
|
||||
}
|
||||
|
||||
private List<ReleaseResource> MapDecisions(IEnumerable<DownloadDecision> decisions)
|
||||
private List<ReleaseResource> MapDecisions(IEnumerable<DownloadDecision> decisions, List<EpisodeHistory> history)
|
||||
{
|
||||
var result = new List<ReleaseResource>();
|
||||
|
||||
|
|
@ -265,9 +271,70 @@ private List<ReleaseResource> MapDecisions(IEnumerable<DownloadDecision> decisio
|
|||
{
|
||||
var release = downloadDecision.MapDecision(result.Count, _qualityProfile);
|
||||
|
||||
release.History = AddHistory(downloadDecision.RemoteEpisode.Release, history);
|
||||
|
||||
result.Add(release);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ReleaseHistoryResource? AddHistory(ReleaseInfo release, List<EpisodeHistory> history)
|
||||
{
|
||||
var grabbed = history.FirstOrDefault(h => h.EventType == EpisodeHistoryEventType.Grabbed &&
|
||||
h.Data.TryGetValue("guid", out var guid) &&
|
||||
guid == release.Guid);
|
||||
|
||||
if (grabbed == null && release.DownloadProtocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
if (release is not TorrentInfo torrentInfo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (torrentInfo.InfoHash.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
grabbed = history.FirstOrDefault(h => h.EventType == EpisodeHistoryEventType.Grabbed &&
|
||||
ReleaseComparer.SameTorrent(new ReleaseComparerModel(h),
|
||||
torrentInfo));
|
||||
}
|
||||
|
||||
if (grabbed == null)
|
||||
{
|
||||
grabbed = history.FirstOrDefault(h => h.EventType == EpisodeHistoryEventType.Grabbed &&
|
||||
h.SourceTitle == release.Title &&
|
||||
(DownloadProtocol)Convert.ToInt32(
|
||||
h.Data.GetValueOrDefault("protocol")) ==
|
||||
DownloadProtocol.Torrent &&
|
||||
ReleaseComparer.SameTorrent(new ReleaseComparerModel(h),
|
||||
torrentInfo));
|
||||
}
|
||||
}
|
||||
else if (grabbed == null)
|
||||
{
|
||||
grabbed = history.FirstOrDefault(h => h.EventType == EpisodeHistoryEventType.Grabbed &&
|
||||
ReleaseComparer.SameNzb(new ReleaseComparerModel(h),
|
||||
release));
|
||||
}
|
||||
|
||||
if (grabbed != null)
|
||||
{
|
||||
var resource = new ReleaseHistoryResource
|
||||
{
|
||||
Grabbed = grabbed.Date,
|
||||
};
|
||||
|
||||
var failedHistory = history.FirstOrDefault(h => h.EventType == EpisodeHistoryEventType.DownloadFailed &&
|
||||
h.DownloadId == grabbed.DownloadId);
|
||||
|
||||
if (failedHistory != null)
|
||||
{
|
||||
resource.Failed = failedHistory.Date;
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
src/Sonarr.Api.V5/Release/ReleaseHistoryResource.cs
Normal file
7
src/Sonarr.Api.V5/Release/ReleaseHistoryResource.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Sonarr.Api.V5.Release;
|
||||
|
||||
public class ReleaseHistoryResource
|
||||
{
|
||||
public DateTime? Grabbed { get; set; }
|
||||
public DateTime? Failed { get; set; }
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ public class ReleaseResource : RestResource
|
|||
public ParsedEpisodeInfoResource? ParsedInfo { get; set; }
|
||||
public ReleaseInfoResource? Release { get; set; }
|
||||
public ReleaseDecisionResource? Decision { get; set; }
|
||||
public ReleaseHistoryResource? History { get; set; }
|
||||
public int QualityWeight { get; set; }
|
||||
public List<Language> Languages { get; set; } = [];
|
||||
public int? MappedSeasonNumber { get; set; }
|
||||
|
|
@ -56,20 +57,6 @@ public static ReleaseResource ToResource(this DownloadDecision model)
|
|||
};
|
||||
}
|
||||
|
||||
public static List<ReleaseResource> MapDecisions(this IEnumerable<DownloadDecision> decisions, QualityProfile profile)
|
||||
{
|
||||
var result = new List<ReleaseResource>();
|
||||
|
||||
foreach (var downloadDecision in decisions)
|
||||
{
|
||||
var release = MapDecision(downloadDecision, result.Count, profile);
|
||||
|
||||
result.Add(release);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ReleaseResource MapDecision(this DownloadDecision decision, int initialWeight, QualityProfile profile)
|
||||
{
|
||||
var release = decision.ToResource();
|
||||
|
|
|
|||
Loading…
Reference in a new issue