Many UI and API Improvements (#8)

This fixes and implements many items on the ArtistIndex Page and ArtistDetailPage

* Create ArtistStatistics Core Module and tie into API.
* Create Members Class and tie into ArtistModel and Artist API resource.
* Finish Out Album API resources and pass to ArtistDetailPage.
* Finish Out Track and TrackFile API resources and pass to ArtistDetailPage.
* Lots of UI work on Artist Detail Page to get Albums and Track list working.
* Add Cover and Disc Image Types to MediaCover Class
* Remove AddSeries UI Flow, since we have replaced with AddArtist (Cleanup)
pull/6/head
Qstick 7 years ago committed by Joseph Milazzo
parent 2c7398ac66
commit d8ea0a3243

@ -16,10 +16,10 @@ gulp.task('less', function() {
paths.src.content + 'bootstrap.less',
paths.src.content + 'theme.less',
paths.src.content + 'overrides.less',
paths.src.root + 'Series/series.less',
//paths.src.root + 'Series/series.less',
paths.src.root + 'Artist/artist.less',
paths.src.root + 'Activity/activity.less',
paths.src.root + 'AddSeries/addSeries.less',
//paths.src.root + 'AddSeries/addSeries.less',
paths.src.root + 'AddArtist/addArtist.less',
paths.src.root + 'Calendar/calendar.less',
paths.src.root + 'Cells/cells.less',

@ -0,0 +1,40 @@
using System.Collections.Generic;
using NzbDrone.Api.REST;
using NzbDrone.Core.Music;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.SignalR;
namespace NzbDrone.Api.Albums
{
public class AlbumModule : AlbumModuleWithSignalR
{
public AlbumModule(IArtistService artistService,
IAlbumService albumService,
IQualityUpgradableSpecification qualityUpgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(albumService, artistService, qualityUpgradableSpecification, signalRBroadcaster)
{
GetResourceAll = GetAlbums;
UpdateResource = SetMonitored;
}
private List<AlbumResource> GetAlbums()
{
if (!Request.Query.ArtistId.HasValue)
{
throw new BadRequestException("artistId is missing");
}
var artistId = (int)Request.Query.ArtistId;
var resources = MapToResource(_albumService.GetAlbumsByArtist(artistId), false);
return resources;
}
private void SetMonitored(AlbumResource albumResource)
{
_albumService.SetAlbumMonitored(albumResource.Id, albumResource.Monitored);
}
}
}

@ -0,0 +1,118 @@
using System.Collections.Generic;
using NzbDrone.Common.Extensions;
using NzbDrone.Api.TrackFiles;
using NzbDrone.Api.Music;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.SignalR;
namespace NzbDrone.Api.Albums
{
public abstract class AlbumModuleWithSignalR : NzbDroneRestModuleWithSignalR<AlbumResource, Track>
{
protected readonly IAlbumService _albumService;
protected readonly IArtistService _artistService;
protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
protected AlbumModuleWithSignalR(IAlbumService albumService,
IArtistService artistService,
IQualityUpgradableSpecification qualityUpgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster)
: base(signalRBroadcaster)
{
_albumService = albumService;
_artistService = artistService;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
GetResourceById = GetAlbum;
}
protected AlbumModuleWithSignalR(IAlbumService albumService,
IArtistService artistService,
IQualityUpgradableSpecification qualityUpgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster,
string resource)
: base(signalRBroadcaster, resource)
{
_albumService = albumService;
_artistService = artistService;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
GetResourceById = GetAlbum;
}
protected AlbumResource GetAlbum(int id)
{
var album = _albumService.GetAlbum(id);
var resource = MapToResource(album, true);
return resource;
}
protected AlbumResource MapToResource(Album album, bool includeArtist)
{
var resource = album.ToResource();
if (includeArtist)
{
var artist = album.Artist ?? _artistService.GetArtist(album.ArtistId);
if (includeArtist)
{
resource.Artist = artist.ToResource();
}
}
return resource;
}
protected List<AlbumResource> MapToResource(List<Album> albums, bool includeArtist)
{
var result = albums.ToResource();
if (includeArtist)
{
var artistDict = new Dictionary<int, Core.Music.Artist>();
for (var i = 0; i < albums.Count; i++)
{
var album = albums[i];
var resource = result[i];
var artist = album.Artist ?? artistDict.GetValueOrDefault(albums[i].ArtistId) ?? _artistService.GetArtist(albums[i].ArtistId);
artistDict[artist.Id] = artist;
if (includeArtist)
{
resource.Artist = artist.ToResource();
}
}
}
return result;
}
//TODO: Implement Track or Album Grabbed/Dowloaded Events
//public void Handle(TrackGrabbedEvent message)
//{
// foreach (var track in message.Track.Tracks)
// {
// var resource = track.ToResource();
// resource.Grabbed = true;
// BroadcastResourceChange(ModelAction.Updated, resource);
// }
//}
//public void Handle(TrackDownloadedEvent message)
//{
// foreach (var album in message.Album.Albums)
// {
// BroadcastResourceChange(ModelAction.Updated, album.Id);
// }
//}
}
}

@ -3,33 +3,47 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Api.REST;
using NzbDrone.Api.Music;
using NzbDrone.Core.MediaCover;
namespace NzbDrone.Api.Music
namespace NzbDrone.Api.Albums
{
public class AlbumResource
public class AlbumResource : RestResource
{
public string AlbumId { get; set; }
public string AlbumName { get; set; }
public string Title { get; set; }
public string Label { get; set; }
public bool Monitored { get; set; }
public string Path { get; set; }
public int ProfileId { get; set; }
public Ratings Ratings { get; set; }
public DateTime ReleaseDate { get; set; }
public List<string> Genres { get; set; }
public string ArtworkUrl { get; set; }
public ArtistResource Artist { get; set; }
public List<MediaCover> Images { get; set; }
}
public static class AlbumResourceMapper
{
public static AlbumResource ToResource(this Album model)
public static AlbumResource ToResource(this Core.Music.Album model)
{
if (model == null) return null;
return new AlbumResource
{
AlbumId = model.ForeignAlbumId,
Id = model.Id,
Label = model.Label,
Path = model.Path,
ProfileId = model.ProfileId,
Monitored = model.Monitored,
ReleaseDate = model.ReleaseDate,
AlbumName = model.Title,
Genres = model.Genres,
Title = model.Title,
Images = model.Images,
Ratings = model.Ratings,
//Genres = model.Genres,
//ArtworkUrl = model.ArtworkUrl
};
}
@ -38,13 +52,19 @@ namespace NzbDrone.Api.Music
{
if (resource == null) return null;
return new Album
return new Core.Music.Album
{
ForeignAlbumId = resource.AlbumId,
Id = resource.Id,
Label = resource.Label,
Path = resource.Path,
Monitored = resource.Monitored,
ProfileId = resource.ProfileId,
ReleaseDate = resource.ReleaseDate,
Title = resource.AlbumName,
Genres = resource.Genres,
Title = resource.Title,
Images = resource.Images,
Ratings = resource.Ratings,
//Genres = resource.Genres,
//ArtworkUrl = resource.ArtworkUrl
};
}

@ -1,4 +1,7 @@
using FluentValidation;
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaCover;
@ -7,12 +10,10 @@ using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Events;
using NzbDrone.Core.SeriesStats;
using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.SignalR;
using System;
using System.Collections.Generic;
namespace NzbDrone.Api.Music
{
@ -26,14 +27,14 @@ namespace NzbDrone.Api.Music
//IHandle<MediaCoversUpdatedEvent>
{
private readonly IArtistService _artistService;
private readonly IAddArtistService _addSeriesService;
private readonly ISeriesStatisticsService _seriesStatisticsService;
private readonly IAddArtistService _addArtistService;
private readonly IArtistStatisticsService _artistStatisticsService;
private readonly IMapCoversToLocal _coverMapper;
public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster,
IArtistService artistService,
IAddArtistService addSeriesService,
ISeriesStatisticsService seriesStatisticsService,
IAddArtistService addArtistService,
IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper,
RootFolderValidator rootFolderValidator,
ArtistPathValidator seriesPathValidator,
@ -45,8 +46,8 @@ namespace NzbDrone.Api.Music
: base(signalRBroadcaster)
{
_artistService = artistService;
_addSeriesService = addSeriesService;
_seriesStatisticsService = seriesStatisticsService;
_addArtistService = addArtistService;
_artistStatisticsService = artistStatisticsService;
_coverMapper = coverMapper;
@ -88,7 +89,7 @@ namespace NzbDrone.Api.Music
var resource = artist.ToResource();
MapCoversToLocal(resource);
//FetchAndLinkSeriesStatistics(resource);
FetchAndLinkArtistStatistics(resource);
//PopulateAlternateTitles(resource);
return resource;
@ -96,10 +97,11 @@ namespace NzbDrone.Api.Music
private List<ArtistResource> AllArtist()
{
//var seriesStats = _seriesStatisticsService.SeriesStatistics();
var artistStats = _artistStatisticsService.ArtistStatistics();
var artistResources = _artistService.GetAllArtists().ToResource();
MapCoversToLocal(artistResources.ToArray());
//LinkSeriesStatistics(seriesResources, seriesStats);
LinkArtistStatistics(artistResources, artistStats);
//PopulateAlternateTitles(seriesResources);
return artistResources;
@ -109,7 +111,7 @@ namespace NzbDrone.Api.Music
{
var model = artistResource.ToModel();
return _addSeriesService.AddArtist(model).Id;
return _addArtistService.AddArtist(model).Id;
}
private void UpdatArtist(ArtistResource artistResource)
@ -142,6 +144,44 @@ namespace NzbDrone.Api.Music
}
}
private void FetchAndLinkArtistStatistics(ArtistResource resource)
{
LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id));
}
private void LinkArtistStatistics(List<ArtistResource> resources, List<ArtistStatistics> artistStatistics)
{
var dictArtistStats = artistStatistics.ToDictionary(v => v.ArtistId);
foreach (var artist in resources)
{
var stats = dictArtistStats.GetValueOrDefault(artist.Id);
if (stats == null) continue;
LinkArtistStatistics(artist, stats);
}
}
private void LinkArtistStatistics(ArtistResource resource, ArtistStatistics artistStatistics)
{
resource.TotalTrackCount = artistStatistics.TotalTrackCount;
resource.TrackCount = artistStatistics.TrackCount;
resource.TrackFileCount = artistStatistics.TrackFileCount;
resource.SizeOnDisk = artistStatistics.SizeOnDisk;
resource.AlbumCount = artistStatistics.AlbumCount;
//if (artistStatistics.AlbumStatistics != null)
//{
// var dictSeasonStats = artistStatistics.SeasonStatistics.ToDictionary(v => v.SeasonNumber);
// foreach (var album in resource.Albums)
// {
// album.Statistics = dictSeasonStats.GetValueOrDefault(album.Id).ToResource();
// }
//}
}
public void Handle(TrackImportedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.Id); // TODO: Ensure we can pass DB ID instead of Metadata ID (SpotifyID)

@ -1,7 +1,8 @@
using NzbDrone.Api.REST;
using NzbDrone.Api.Series;
using NzbDrone.Api.Albums;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Music;
using System;
using System.Collections.Generic;
using System.Linq;
@ -23,19 +24,10 @@ namespace NzbDrone.Api.Music
public string MBId { get; set; }
public int TADBId { get; set; }
public int DiscogsId { get; set; }
public string AMId { get; set; }
public string AllMusicId { get; set; }
public string Overview { get; set; }
public int AlbumCount
{
get
{
if (Albums == null) return 0;
return Albums.Where(s => s.AlbumId != "").Count(); // TODO: CHeck this condition
}
}
public int? AlbumCount{ get; set; }
public int? TotalTrackCount { get; set; }
public int? TrackCount { get; set; }
public int? TrackFileCount { get; set; }
@ -43,6 +35,7 @@ namespace NzbDrone.Api.Music
//public SeriesStatusType Status { get; set; }
public List<MediaCover> Images { get; set; }
public List<Member> Members { get; set; }
public string RemotePoster { get; set; }
public List<AlbumResource> Albums { get; set; }
@ -61,7 +54,7 @@ namespace NzbDrone.Api.Music
public List<string> Genres { get; set; }
public HashSet<int> Tags { get; set; }
public DateTime Added { get; set; }
public AddSeriesOptions AddOptions { get; set; }
public AddArtistOptions AddOptions { get; set; }
public Ratings Ratings { get; set; }
public string NameSlug { get; set; }
}
@ -78,14 +71,14 @@ namespace NzbDrone.Api.Music
MBId = model.MBId,
TADBId = model.TADBId,
DiscogsId = model.DiscogsId,
AMId = model.AMId,
AllMusicId = model.AMId,
Name = model.Name,
//AlternateTitles
//SortTitle = resource.SortTitle,
//TotalEpisodeCount
//EpisodeCount
//EpisodeFileCount
//TotalTrackCount
//TrackCount
//TrackFileCount
//SizeOnDisk
//Status = resource.Status,
Overview = model.Overview,
@ -94,6 +87,7 @@ namespace NzbDrone.Api.Music
//Network = resource.Network,
//AirTime = resource.AirTime,
Images = model.Images,
Members = model.Members,
Albums = model.Albums.ToResource(),
//Year = resource.Year,
@ -119,7 +113,7 @@ namespace NzbDrone.Api.Music
Tags = model.Tags,
Added = model.Added,
AddOptions = model.AddOptions,
//Ratings = resource.Ratings
Ratings = model.Ratings,
};
}
@ -137,10 +131,10 @@ namespace NzbDrone.Api.Music
MBId = resource.MBId,
TADBId = resource.TADBId,
DiscogsId = resource.DiscogsId,
AMId = resource.AMId,
AMId = resource.AllMusicId, //TODO change model and DB to AllMusic instead of AM
//TotalEpisodeCount
//EpisodeCount
//EpisodeFileCount
//TrackCount
//TrackFileCount
//SizeOnDisk
//Status = resource.Status,
Overview = resource.Overview,
@ -149,6 +143,7 @@ namespace NzbDrone.Api.Music
//Network = resource.Network,
//AirTime = resource.AirTime,
Images = resource.Images,
Members = resource.Members,
Albums = resource.Albums.ToModel(),
//Year = resource.Year,
@ -166,7 +161,7 @@ namespace NzbDrone.Api.Music
Tags = resource.Tags,
Added = resource.Added,
AddOptions = resource.AddOptions,
//Ratings = resource.Ratings
Ratings = resource.Ratings
};
}

@ -88,6 +88,7 @@
<Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Albums\AlbumModuleWithSignalR.cs" />
<Compile Include="Authentication\AuthenticationService.cs" />
<Compile Include="Authentication\EnableAuthInNancy.cs" />
<Compile Include="Authentication\AuthenticationModule.cs" />
@ -106,6 +107,7 @@
<Compile Include="Commands\CommandResource.cs" />
<Compile Include="TrackFiles\TrackFileModule.cs" />
<Compile Include="TrackFiles\TrackFileResource.cs" />
<Compile Include="Albums\AlbumModule.cs" />
<Compile Include="Tracks\TrackModule.cs" />
<Compile Include="Tracks\TrackModuleWithSignalR.cs" />
<Compile Include="Tracks\TrackResource.cs" />
@ -118,7 +120,7 @@
<Compile Include="Frontend\Mappers\RobotsTxtMapper.cs" />
<Compile Include="Indexers\ReleaseModuleBase.cs" />
<Compile Include="Indexers\ReleasePushModule.cs" />
<Compile Include="Music\AlbumResource.cs" />
<Compile Include="Albums\AlbumResource.cs" />
<Compile Include="Music\ArtistLookupModule.cs" />
<Compile Include="Music\ArtistModule.cs" />
<Compile Include="Music\ArtistResource.cs" />

@ -14,7 +14,8 @@ namespace NzbDrone.Api.Tracks
public int ArtistId { get; set; }
public int TrackFileId { get; set; }
public int AlbumId { get; set; }
//public int EpisodeNumber { get; set; }
public bool Explicit { get; set; }
public int TrackNumber { get; set; }
public string Title { get; set; }
//public string AirDate { get; set; }
//public DateTime? AirDateUtc { get; set; }
@ -30,6 +31,7 @@ namespace NzbDrone.Api.Tracks
//public bool UnverifiedSceneNumbering { get; set; }
//public string SeriesTitle { get; set; }
public ArtistResource Artist { get; set; }
public Ratings Ratings { get; set; }
//Hiding this so people don't think its usable (only used to set the initial state)
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
@ -49,7 +51,8 @@ namespace NzbDrone.Api.Tracks
ArtistId = model.ArtistId,
TrackFileId = model.TrackFileId,
AlbumId = model.AlbumId,
//EpisodeNumber = model.EpisodeNumber,
Explicit = model.Explicit,
TrackNumber = model.TrackNumber,
Title = model.Title,
//AirDate = model.AirDate,
//AirDateUtc = model.AirDateUtc,
@ -58,6 +61,7 @@ namespace NzbDrone.Api.Tracks
HasFile = model.HasFile,
Monitored = model.Monitored,
Ratings = model.Ratings,
//AbsoluteEpisodeNumber = model.AbsoluteEpisodeNumber,
//SceneAbsoluteEpisodeNumber = model.SceneAbsoluteEpisodeNumber,
//SceneEpisodeNumber = model.SceneEpisodeNumber,

@ -0,0 +1,15 @@
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.ArtistStats
{
public class AlbumStatistics : ResultSet
{
public int ArtistId { get; set; }
public int AlbumId { get; set; }
public int TrackFileCount { get; set; }
public int TrackCount { get; set; }
public int TotalTrackCount { get; set; }
public long SizeOnDisk { get; set; }
}
}

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.ArtistStats
{
public class ArtistStatistics : ResultSet
{
public int ArtistId { get; set; }
public int AlbumCount { get; set; }
public int TrackFileCount { get; set; }
public int TrackCount { get; set; }
public int TotalTrackCount { get; set; }
public long SizeOnDisk { get; set; }
public List<AlbumStatistics> AlbumStatistics { get; set; }
}
}

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Text;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.ArtistStats
{
public interface IArtistStatisticsRepository
{
List<AlbumStatistics> ArtistStatistics();
List<AlbumStatistics> ArtistStatistics(int artistId);
}
public class ArtistStatisticsRepository : IArtistStatisticsRepository
{
private readonly IMainDatabase _database;
public ArtistStatisticsRepository(IMainDatabase database)
{
_database = database;
}
public List<AlbumStatistics> ArtistStatistics()
{
var mapper = _database.GetDataMapper();
mapper.AddParameter("currentDate", DateTime.UtcNow);
var sb = new StringBuilder();
sb.AppendLine(GetSelectClause());
sb.AppendLine(GetTrackFilesJoin());
sb.AppendLine(GetGroupByClause());
var queryText = sb.ToString();
return mapper.Query<AlbumStatistics>(queryText);
}
public List<AlbumStatistics> ArtistStatistics(int artistId)
{
var mapper = _database.GetDataMapper();
mapper.AddParameter("currentDate", DateTime.UtcNow);
mapper.AddParameter("artistId", artistId);
var sb = new StringBuilder();
sb.AppendLine(GetSelectClause());
sb.AppendLine(GetTrackFilesJoin());
sb.AppendLine("WHERE Tracks.ArtistId = @artistId");
sb.AppendLine(GetGroupByClause());
var queryText = sb.ToString();
return mapper.Query<AlbumStatistics>(queryText);
}
private string GetSelectClause()
{
return @"SELECT Tracks.*, SUM(TrackFiles.Size) as SizeOnDisk FROM
(SELECT
Tracks.ArtistId,
Tracks.AlbumId,
SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS TotalTrackCount,
SUM(CASE WHEN Monitored = 1 OR TrackFileId > 0 THEN 1 ELSE 0 END) AS TrackCount,
SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS TrackFileCount
FROM Tracks
GROUP BY Tracks.ArtistId, Tracks.AlbumId) as Tracks";
}
private string GetGroupByClause()
{
return "GROUP BY Tracks.ArtistId, Tracks.AlbumId";
}
private string GetTrackFilesJoin()
{
return @"LEFT OUTER JOIN TrackFiles
ON TrackFiles.ArtistId = Tracks.ArtistId
AND TrackFiles.AlbumId = Tracks.AlbumId";
}
}
}

@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.ArtistStats
{
public interface IArtistStatisticsService
{
List<ArtistStatistics> ArtistStatistics();
ArtistStatistics ArtistStatistics(int artistId);
}
public class ArtistStatisticsService : IArtistStatisticsService
{
private readonly IArtistStatisticsRepository _artistStatisticsRepository;
public ArtistStatisticsService(IArtistStatisticsRepository artistStatisticsRepository)
{
_artistStatisticsRepository = artistStatisticsRepository;
}
public List<ArtistStatistics> ArtistStatistics()
{
var albumStatistics = _artistStatisticsRepository.ArtistStatistics();
return albumStatistics.GroupBy(s => s.ArtistId).Select(s => MapArtistStatistics(s.ToList())).ToList();
}
public ArtistStatistics ArtistStatistics(int artistId)
{
var stats = _artistStatisticsRepository.ArtistStatistics(artistId);
if (stats == null || stats.Count == 0) return new ArtistStatistics();
return MapArtistStatistics(stats);
}
private ArtistStatistics MapArtistStatistics(List<AlbumStatistics> albumStatistics)
{
var artistStatistics = new ArtistStatistics
{
AlbumStatistics = albumStatistics,
AlbumCount = albumStatistics.Count,
ArtistId = albumStatistics.First().ArtistId,
TrackFileCount = albumStatistics.Sum(s => s.TrackFileCount),
TrackCount = albumStatistics.Sum(s => s.TrackCount),
TotalTrackCount = albumStatistics.Sum(s => s.TotalTrackCount),
SizeOnDisk = albumStatistics.Sum(s => s.SizeOnDisk)
};
return artistStatistics;
}
}
}

@ -23,6 +23,7 @@ using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Restrictions;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.SeriesStats;
using NzbDrone.Core.Tags;
using NzbDrone.Core.ThingiProvider;
@ -121,7 +122,7 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<Profile>().RegisterModel("Profiles");
Mapper.Entity<Log>().RegisterModel("Logs");
Mapper.Entity<NamingConfig>().RegisterModel("NamingConfig");
Mapper.Entity<SeasonStatistics>().MapResultSet();
Mapper.Entity<AlbumStatistics>().MapResultSet();
Mapper.Entity<Blacklist>().RegisterModel("Blacklist");
Mapper.Entity<MetadataFile>().RegisterModel("MetadataFiles");
Mapper.Entity<SubtitleFile>().RegisterModel("SubtitleFiles");

@ -1,4 +1,4 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.MediaCover
{
@ -10,7 +10,9 @@ namespace NzbDrone.Core.MediaCover
Banner = 2,
Fanart = 3,
Screenshot = 4,
Headshot = 5
Headshot = 5,
Cover = 6,
Disc = 7
}
public class MediaCover : IEmbeddedDocument

@ -365,7 +365,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
series.TitleSlug = show.Slug;
series.Status = MapSeriesStatus(show.Status);
series.Ratings = MapRatings(show.Rating);
//series.Ratings = MapRatings(show.Rating);
series.Genres = show.Genres;
if (show.ContentRating.IsNotNullOrWhiteSpace())
@ -412,7 +412,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
episode.AirDate = oracleEpisode.AirDate;
episode.AirDateUtc = oracleEpisode.AirDateUtc;
episode.Ratings = MapRatings(oracleEpisode.Rating);
//episode.Ratings = MapRatings(oracleEpisode.Rating);
//Don't include series fanart images as episode screenshot
if (oracleEpisode.Image != null)
@ -443,14 +443,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
return SeriesStatusType.Continuing;
}
private static Ratings MapRatings(RatingResource rating)
private static Core.Music.Ratings MapRatings(RatingResource rating)
{
if (rating == null)
{
return new Ratings();
return new Core.Music.Ratings();
}
return new Ratings
return new Core.Music.Ratings
{
Votes = rating.Count,
Value = rating.Value

@ -17,21 +17,25 @@ namespace NzbDrone.Core.Music
public string ForeignAlbumId { get; set; }
public int ArtistId { get; set; }
public string Title { get; set; } // NOTE: This should be CollectionName in API
public string Title { get; set; }
public string CleanTitle { get; set; }
public DateTime ReleaseDate { get; set; }
public string Label { get; set; }
//public int TrackCount { get; set; }
public string Path { get; set; }
public int ProfileId { get; set; }
public List<Track> Tracks { get; set; }
//public int DiscCount { get; set; }
public bool Monitored { get; set; }
public List<MediaCover.MediaCover> Images { get; set; }
//public List<Actor> Actors { get; set; } // These are band members. TODO: Refactor
//public List<Actor> Actors { get; set; } // TODO: These are band members. TODO: Refactor
public List<string> Genres { get; set; }
public String AlbumType { get; set; } //Turn this into a type similar to Series Type in TV
public String AlbumType { get; set; } // TODO: Turn this into a type similar to Series Type in TV
//public string ArtworkUrl { get; set; }
//public string Explicitness { get; set; }
public AddSeriesOptions AddOptions { get; set; }
public Artist Artist { get; set; }
public Ratings Ratings { get; set; }
public override string ToString()
{

@ -11,6 +11,7 @@ namespace NzbDrone.Core.Music
List<Album> GetAlbums(int artistId);
Album FindByName(string cleanTitle);
Album FindById(string spotifyId);
void SetMonitoredFlat(Album album, bool monitored);
}
public class AlbumRepository : BasicRepository<Album>, IAlbumRepository
@ -35,6 +36,12 @@ namespace NzbDrone.Core.Music
return Query.Where(s => s.ForeignAlbumId == foreignAlbumId).SingleOrDefault();
}
public void SetMonitoredFlat(Album album, bool monitored)
{
album.Monitored = monitored;
SetFields(album, p => p.Monitored);
}
public Album FindByName(string cleanTitle)
{
cleanTitle = cleanTitle.ToLowerInvariant();

@ -24,6 +24,7 @@ namespace NzbDrone.Core.Music
List<Album> GetAllAlbums();
Album UpdateAlbum(Album album);
List<Album> UpdateAlbums(List<Album> album);
void SetAlbumMonitored(int albumId, bool monitored);
void InsertMany(List<Album> albums);
void UpdateMany(List<Album> albums);
void DeleteMany(List<Album> albums);
@ -134,6 +135,14 @@ namespace NzbDrone.Core.Music
return updatedAlbum;
}
public void SetAlbumMonitored(int albumId, bool monitored)
{
var album = _albumRepository.Get(albumId);
_albumRepository.SetMonitoredFlat(album, monitored);
_logger.Debug("Monitored flag for Album:{0} was set to {1}", albumId, monitored);
}
public List<Album> UpdateAlbums(List<Album> album)
{
_logger.Debug("Updating {0} album", album.Count);

@ -2,7 +2,7 @@
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Music;
using System;
using System.Collections.Generic;
using System.Linq;
@ -16,7 +16,7 @@ namespace NzbDrone.Core.Music
{
Images = new List<MediaCover.MediaCover>();
Genres = new List<string>();
//Members = new List<Person>(); // Artist Band Member? (NOTE: This should be per album)
Members = new List<Member>();
Albums = new List<Album>();
Tags = new HashSet<int>();
@ -45,7 +45,9 @@ namespace NzbDrone.Core.Music
public int ProfileId { get; set; }
public List<Album> Albums { get; set; }
public HashSet<int> Tags { get; set; }
public AddSeriesOptions AddOptions { get; set; }
public AddArtistOptions AddOptions { get; set; }
public Ratings Ratings { get; set; }
public List<Member> Members { get; set; }
public override string ToString()
{
@ -76,6 +78,8 @@ namespace NzbDrone.Core.Music
Albums = otherArtist.Albums;
Tags = otherArtist.Tags;
AddOptions = otherArtist.AddOptions;
Ratings = otherArtist.Ratings;
Members = otherArtist.Members;
Albums = otherArtist.Albums;
Path = otherArtist.Path;
@ -85,6 +89,7 @@ namespace NzbDrone.Core.Music
RootFolderPath = otherArtist.RootFolderPath;
Tags = otherArtist.Tags;
AddOptions = otherArtist.AddOptions;
}
}

@ -0,0 +1,17 @@
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Music
{
public class Member : IEmbeddedDocument
{
public Member()
{
Images = new List<MediaCover.MediaCover>();
}
public string Name { get; set; }
public string Instrument { get; set; }
public List<MediaCover.MediaCover> Images { get; set; }
}
}

@ -0,0 +1,10 @@
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Music
{
public class Ratings : IEmbeddedDocument
{
public int Votes { get; set; }
public decimal Value { get; set; }
}
}

@ -30,6 +30,7 @@ namespace NzbDrone.Core.Music
public bool Explicit { get; set; }
public bool Monitored { get; set; }
public int TrackFileId { get; set; }
public Ratings Ratings { get; set; }
//public DateTime? ReleaseDate { get; set; }
public LazyLoaded<TrackFile> TrackFile { get; set; }

@ -859,6 +859,7 @@
<Compile Include="Extras\Metadata\MetadataType.cs" />
<Compile Include="MetadataSource\IProvideSeriesInfo.cs" />
<Compile Include="MetadataSource\ISearchForNewSeries.cs" />
<Compile Include="Music\Member.cs" />
<Compile Include="Music\AddArtistOptions.cs" />
<Compile Include="Music\AddArtistService.cs" />
<Compile Include="Music\AddArtistValidator.cs" />
@ -882,6 +883,7 @@
<Compile Include="Music\Events\ArtistUpdatedEvent.cs" />
<Compile Include="Music\Events\AlbumInfoRefreshedEvent.cs" />
<Compile Include="Music\Events\TrackInfoRefreshedEvent.cs" />
<Compile Include="Music\Ratings.cs" />
<Compile Include="Music\RefreshArtistService.cs" />
<Compile Include="Music\RefreshAlbumService.cs" />
<Compile Include="Music\RefreshTrackService.cs" />
@ -1104,6 +1106,10 @@
</Compile>
<Compile Include="RootFolders\UnmappedFolder.cs" />
<Compile Include="Security.cs" />
<Compile Include="ArtistStats\AlbumStatistics.cs" />
<Compile Include="ArtistStats\ArtistStatistics.cs" />
<Compile Include="ArtistStats\ArtistStatisticsRepository.cs" />
<Compile Include="ArtistStats\ArtistStatisticsService.cs" />
<Compile Include="SeriesStats\SeasonStatistics.cs" />
<Compile Include="SeriesStats\SeriesStatistics.cs" />
<Compile Include="SeriesStats\SeriesStatisticsRepository.cs" />

@ -1,23 +0,0 @@
var Backbone = require('backbone');
var ArtistModel = require('../Artist/ArtistModel');
var _ = require('underscore');
module.exports = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/artist/lookup',
model : ArtistModel,
parse : function(response) {
var self = this;
_.each(response, function(model) {
model.id = undefined;
if (self.unmappedFolderModel) {
model.path = self.unmappedFolderModel.get('folder').path;
}
});
console.log('response: ', response);
return response;
}
});

@ -1,53 +0,0 @@
var vent = require('vent');
var AppLayout = require('../AppLayout');
var Marionette = require('marionette');
var RootFolderLayout = require('./RootFolders/RootFolderLayout');
var ExistingSeriesCollectionView = require('./Existing/AddExistingSeriesCollectionView');
var AddSeriesView = require('./AddSeriesView');
var ProfileCollection = require('../Profile/ProfileCollection');
var RootFolderCollection = require('./RootFolders/RootFolderCollection');
require('../Artist/ArtistCollection');
module.exports = Marionette.Layout.extend({
template : 'AddSeries/AddSeriesLayoutTemplate',
regions : {
workspace : '#add-series-workspace'
},
events : {
'click .x-import' : '_importSeries',
'click .x-add-new' : '_addSeries'
},
attributes : {
id : 'add-series-screen'
},
initialize : function() {
ProfileCollection.fetch();
RootFolderCollection.fetch().done(function() {
RootFolderCollection.synced = true;
});
},
onShow : function() {
this.workspace.show(new AddSeriesView());
},
_folderSelected : function(options) {
vent.trigger(vent.Commands.CloseModalCommand);
this.workspace.show(new ExistingSeriesCollectionView({ model : options.model }));
},
_importSeries : function() {
this.rootFolderLayout = new RootFolderLayout();
this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected);
AppLayout.modalRegion.show(this.rootFolderLayout);
},
_addSeries : function() {
this.workspace.show(new AddSeriesView());
}
});

@ -1,17 +0,0 @@
<div class="row">
<div class="col-md-12">
<div class="btn-group add-series-btn-group btn-group-lg btn-block">
<button type="button" class="btn btn-default col-md-10 col-xs-8 add-series-import-btn x-import">
<i class="icon-lidarr-hdd"/>
Import existing artists on disk
</button>
<button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-lidarr-active hidden-xs"></i> Add New Artist</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="add-series-workspace"></div>
</div>
</div>

@ -1,183 +0,0 @@
var _ = require('underscore');
var vent = require('vent');
var Marionette = require('marionette');
var AddSeriesCollection = require('./AddSeriesCollection');
var SearchResultCollectionView = require('./SearchResultCollectionView');
var EmptyView = require('./EmptyView');
var NotFoundView = require('./NotFoundView');
var ErrorView = require('./ErrorView');
var LoadingView = require('../Shared/LoadingView');
module.exports = Marionette.Layout.extend({
template : 'AddSeries/AddSeriesViewTemplate',
regions : {
searchResult : '#search-result'
},
ui : {
seriesSearch : '.x-series-search',
searchBar : '.x-search-bar',
loadMore : '.x-load-more'
},
events : {
'click .x-load-more' : '_onLoadMore'
},
initialize : function(options) {
this.isExisting = options.isExisting;
this.collection = new AddSeriesCollection();
console.log('this.collection:', this.collection);
if (this.isExisting) {
this.collection.unmappedFolderModel = this.model;
}
if (this.isExisting) {
this.className = 'existing-series';
} else {
this.className = 'new-series';
}
this.listenTo(vent, vent.Events.SeriesAdded, this._onSeriesAdded);
this.listenTo(this.collection, 'sync', this._showResults);
this.resultCollectionView = new SearchResultCollectionView({
collection : this.collection,
isExisting : this.isExisting
});
this.throttledSearch = _.debounce(this.search, 1000, { trailing : true }).bind(this);
},
onRender : function() {
var self = this;
this.$el.addClass(this.className);
this.ui.seriesSearch.keyup(function(e) {
if (_.contains([
9,
16,
17,
18,
19,
20,
33,
34,
35,
36,
37,
38,
39,
40,
91,
92,
93
], e.keyCode)) {
return;
}
self._abortExistingSearch();
self.throttledSearch({
term : self.ui.seriesSearch.val()
});
});
this._clearResults();
if (this.isExisting) {
this.ui.searchBar.hide();
}
},
onShow : function() {
this.ui.seriesSearch.focus();
},
search : function(options) {
var self = this;
this.collection.reset();
if (!options.term || options.term === this.collection.term) {
return Marionette.$.Deferred().resolve();
}
this.searchResult.show(new LoadingView());
this.collection.term = options.term;
this.currentSearchPromise = this.collection.fetch({
data : { term : options.term }
});
this.currentSearchPromise.fail(function() {
self._showError();
});
return this.currentSearchPromise;
},
_onSeriesAdded : function(options) {
if (this.isExisting && options.series.get('path') === this.model.get('folder').path) {
this.close();
}
else if (!this.isExisting) {
this.collection.term = '';
this.collection.reset();
this._clearResults();
this.ui.seriesSearch.val('');
this.ui.seriesSearch.focus();
}
},
_onLoadMore : function() {
var showingAll = this.resultCollectionView.showMore();
this.ui.searchBar.show();
if (showingAll) {
this.ui.loadMore.hide();
}
},
_clearResults : function() {
if (!this.isExisting) {
this.searchResult.show(new EmptyView());
} else {
this.searchResult.close();
}
},
_showResults : function() {
if (!this.isClosed) {
if (this.collection.length === 0) {
this.ui.searchBar.show();
this.searchResult.show(new NotFoundView({ term : this.collection.term }));
} else {
this.searchResult.show(this.resultCollectionView);
if (!this.showingAll && this.isExisting) {
this.ui.loadMore.show();
}
}
}
},
_abortExistingSearch : function() {
if (this.currentSearchPromise && this.currentSearchPromise.readyState > 0 && this.currentSearchPromise.readyState < 4) {
console.log('aborting previous pending search request.');
this.currentSearchPromise.abort();
} else {
this._clearResults();
}
},
_showError : function() {
if (!this.isClosed) {
this.ui.searchBar.show();
this.searchResult.show(new ErrorView({ term : this.collection.term }));
this.collection.term = '';
}
}
});

@ -1,24 +0,0 @@
{{#if folder.path}}
<div class="unmapped-folder-path">
<div class="col-md-12">
{{folder.path}}
</div>
</div>{{/if}}
<div class="x-search-bar">
<div class="input-group input-group-lg add-series-search">
<span class="input-group-addon"><i class="icon-lidarr-search"/></span>
{{#if folder}}
<input type="text" class="form-control x-series-search" value="{{folder.name}}">
{{else}}
<input type="text" class="form-control x-series-search" placeholder="Start typing the name of an artist or album...">
{{/if}}
</div>
</div>
<div class="row">
<div id="search-result" class="result-list col-md-12"/>
</div>
<div class="btn btn-block text-center new-series-loadmore x-load-more" style="display: none;">
<i class="icon-lidarr-load-more"/>
more
</div>

@ -1,5 +0,0 @@
var Marionette = require('marionette');
module.exports = Marionette.CompositeView.extend({
template : 'AddSeries/EmptyViewTemplate'
});

@ -1,3 +0,0 @@
<div class="text-center hint col-md-12">
<span>You can also search by iTunes using the itunes: prefixes.</span>
</div>

@ -1,13 +0,0 @@
var Marionette = require('marionette');
module.exports = Marionette.CompositeView.extend({
template : 'AddSeries/ErrorViewTemplate',
initialize : function(options) {
this.options = options;
},
templateHelpers : function() {
return this.options;
}
});

@ -1,7 +0,0 @@
<div class="text-center col-md-12">
<h3>
There was an error searching for '{{term}}'.
</h3>
If the artist name contains non-alphanumeric characters try removing them, otherwise try your search again later.
</div>

@ -1,51 +0,0 @@
var Marionette = require('marionette');
var AddSeriesView = require('../AddSeriesView');
var UnmappedFolderCollection = require('./UnmappedFolderCollection');
module.exports = Marionette.CompositeView.extend({
itemView : AddSeriesView,
itemViewContainer : '.x-loading-folders',
template : 'AddSeries/Existing/AddExistingSeriesCollectionViewTemplate',
ui : {
loadingFolders : '.x-loading-folders'
},
initialize : function() {
this.collection = new UnmappedFolderCollection();
this.collection.importItems(this.model);
},
showCollection : function() {
this._showAndSearch(0);
},
appendHtml : function(collectionView, itemView, index) {
collectionView.ui.loadingFolders.before(itemView.el);
},
_showAndSearch : function(index) {
var self = this;
var model = this.collection.at(index);
if (model) {
var currentIndex = index;
var folderName = model.get('folder').name;
this.addItemView(model, this.getItemView(), index);
this.children.findByModel(model).search({ term : folderName }).always(function() {
if (!self.isClosed) {
self._showAndSearch(currentIndex + 1);
}
});
}
else {
this.ui.loadingFolders.hide();
}
},
itemViewOptions : {
isExisting : true
}
});

@ -1,5 +0,0 @@
<div class="x-existing-folders">
<div class="loading-folders x-loading-folders">
Loading search results from iTunes for your artists, this may take a few minutes.
</div>
</div>

@ -1,20 +0,0 @@
var Backbone = require('backbone');
var UnmappedFolderModel = require('./UnmappedFolderModel');
var _ = require('underscore');
module.exports = Backbone.Collection.extend({
model : UnmappedFolderModel,
importItems : function(rootFolderModel) {
this.reset();
var rootFolder = rootFolderModel;
_.each(rootFolderModel.get('unmappedFolders'), function(folder) {
this.push(new UnmappedFolderModel({
rootFolder : rootFolder,
folder : folder
}));
}, this);
}
});

@ -1,3 +0,0 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({});

@ -1,18 +0,0 @@
<dl class="monitor-tooltip-contents">
<dt>All</dt>
<dd>Monitor all episodes except specials</dd>
<dt>Future</dt>
<dd>Monitor episodes that have not aired yet</dd>
<dt>Missing</dt>
<dd>Monitor episodes that do not have files or have not aired yet</dd>
<dt>Existing</dt>
<dd>Monitor episodes that have files or have not aired yet</dd>
<dt>First Season</dt>
<dd>Monitor all episodes of the first season. All other seasons will be ignored</dd>
<dt>Latest Season</dt>
<dd>Monitor all episodes of the latest season and future seasons</dd>
<dt>None</dt>
<dd>No episodes will be monitored.</dd>
<!--<dt>Latest Season</dt>-->
<!--<dd>Monitor all episodes the latest season only, previous seasons will be ignored</dd>-->
</dl>

@ -1,13 +0,0 @@
var Marionette = require('marionette');
module.exports = Marionette.CompositeView.extend({
template : 'AddSeries/NotFoundViewTemplate',
initialize : function(options) {
this.options = options;
},
templateHelpers : function() {
return this.options;
}
});

@ -1,7 +0,0 @@
<div class="text-center col-md-12">
<h3>
Sorry. We couldn't find any series matching '{{term}}'
</h3>
<a href="https://github.com/NzbDrone/NzbDrone/wiki/FAQ#wiki-why-cant-i-add-a-new-show-to-nzbdrone-its-on-thetvdb">Why can't I find my show?</a>
</div>

@ -1,10 +0,0 @@
var Backbone = require('backbone');
var RootFolderModel = require('./RootFolderModel');
require('../../Mixins/backbone.signalr.mixin');
var RootFolderCollection = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/rootfolder',
model : RootFolderModel
});
module.exports = new RootFolderCollection();

@ -1,8 +0,0 @@
var Marionette = require('marionette');
var RootFolderItemView = require('./RootFolderItemView');
module.exports = Marionette.CompositeView.extend({
template : 'AddSeries/RootFolders/RootFolderCollectionViewTemplate',
itemViewContainer : '.x-root-folders',
itemView : RootFolderItemView
});

@ -1,13 +0,0 @@
<table class="table table-hover">
<thead>
<tr>
<th class="col-md-10 ">
Path
</th>
<th class="col-md-3">
Free Space
</th>
</tr>
</thead>
<tbody class="x-root-folders"></tbody>
</table>

@ -1,28 +0,0 @@
var Marionette = require('marionette');
module.exports = Marionette.ItemView.extend({
template : 'AddSeries/RootFolders/RootFolderItemViewTemplate',
className : 'recent-folder',
tagName : 'tr',
initialize : function() {
this.listenTo(this.model, 'change', this.render);
},
events : {
'click .x-delete' : 'removeFolder',
'click .x-folder' : 'folderSelected'
},
removeFolder : function() {
var self = this;
this.model.destroy().success(function() {
self.close();
});
},
folderSelected : function() {
this.trigger('folderSelected', this.model);
}
});

@ -1,9 +0,0 @@
<td class="col-md-10 x-folder folder-path">
{{path}}
</td>
<td class="col-md-3 x-folder folder-free-space">
<span>{{Bytes freeSpace}}</span>
</td>
<td class="col-md-1">
<i class="icon-lidarr-delete x-delete"></i>
</td>

@ -1,80 +0,0 @@
var Marionette = require('marionette');
var RootFolderCollectionView = require('./RootFolderCollectionView');
var RootFolderCollection = require('./RootFolderCollection');
var RootFolderModel = require('./RootFolderModel');
var LoadingView = require('../../Shared/LoadingView');
var AsValidatedView = require('../../Mixins/AsValidatedView');
require('../../Mixins/FileBrowser');
var Layout = Marionette.Layout.extend({
template : 'AddSeries/RootFolders/RootFolderLayoutTemplate',
ui : {
pathInput : '.x-path'
},
regions : {
currentDirs : '#current-dirs'
},
events : {
'click .x-add' : '_addFolder',
'keydown .x-path input' : '_keydown'
},
initialize : function() {
this.collection = RootFolderCollection;
this.rootfolderListView = null;
},
onShow : function() {
this.listenTo(RootFolderCollection, 'sync', this._showCurrentDirs);
this.currentDirs.show(new LoadingView());
if (RootFolderCollection.synced) {
this._showCurrentDirs();
}
this.ui.pathInput.fileBrowser();
},
_onFolderSelected : function(options) {
this.trigger('folderSelected', options);
},
_addFolder : function() {
var self = this;
var newDir = new RootFolderModel({
Path : this.ui.pathInput.val()
});
this.bindToModelValidation(newDir);
newDir.save().done(function() {
RootFolderCollection.add(newDir);
self.trigger('folderSelected', { model : newDir });
});
},
_showCurrentDirs : function() {
if (!this.rootfolderListView) {
this.rootfolderListView = new RootFolderCollectionView({ collection : RootFolderCollection });
this.currentDirs.show(this.rootfolderListView);
this.listenTo(this.rootfolderListView, 'itemview:folderSelected', this._onFolderSelected);
}
},
_keydown : function(e) {
if (e.keyCode !== 13) {
return;
}
this._addFolder();
}
});
var Layout = AsValidatedView.apply(Layout);
module.exports = Layout;

@ -1,36 +0,0 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Select Folder</h3>
</div>
<div class="modal-body root-folders-modal">
<div class="validation-errors"></div>
<div class="alert alert-info">Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import<button type="button" class="close" data-dismiss="alert">×</button></div>
<div class="row">
<div class="form-group">
<div class="col-md-12">
<div class="input-group">
<span class="input-group-addon">&nbsp;<i class="icon-lidarr-folder-open"></i></span>
<input class="form-control x-path" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows">
<span class="input-group-btn"><button class="btn btn-success x-add"><i class="icon-lidarr-ok"/></button></span>
</div>
</div>
</div>
</div>
<div class="row root-folders">
<div class="col-md-12">
{{#if items}}
<h4>Recent Folders</h4>
{{/if}}
<div id="current-dirs" class="root-folders-list"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal">Close</button>
</div>
</div>

@ -1,8 +0,0 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({
urlRoot : window.NzbDrone.ApiRoot + '/rootfolder',
defaults : {
freeSpace : 0
}
});

@ -1,11 +0,0 @@
<select class="col-md-4 form-control x-root-folder" validation-name="RootFolderPath">
{{#if this}}
{{#each this}}
<option value="{{id}}">{{path}}</option>
{{/each}}
{{else}}
<option value="">Select Path</option>
{{/if}}
<option value="addNew">Add a different path</option>
</select>

@ -1,29 +0,0 @@
var Marionette = require('marionette');
var SearchResultView = require('./SearchResultView');
module.exports = Marionette.CollectionView.extend({
itemView : SearchResultView,
initialize : function(options) {
this.isExisting = options.isExisting;
this.showing = 1;
},
showAll : function() {
this.showingAll = true;
this.render();
},
showMore : function() {
this.showing += 5;
this.render();
return this.showing >= this.collection.length;
},
appendHtml : function(collectionView, itemView, index) {
if (!this.isExisting || index < this.showing || index === 0) {
collectionView.$el.append(itemView.el);
}
}
});

@ -1,297 +0,0 @@
var _ = require('underscore');
var vent = require('vent');
var AppLayout = require('../AppLayout');
var Backbone = require('backbone');
var Marionette = require('marionette');
var Profiles = require('../Profile/ProfileCollection');
var RootFolders = require('./RootFolders/RootFolderCollection');
var RootFolderLayout = require('./RootFolders/RootFolderLayout');
var ArtistCollection = require('../Artist/ArtistCollection');
var Config = require('../Config');
var Messenger = require('../Shared/Messenger');
var AsValidatedView = require('../Mixins/AsValidatedView');
require('jquery.dotdotdot');
var view = Marionette.ItemView.extend({
template : 'AddSeries/SearchResultViewTemplate',
ui : {
profile : '.x-profile',
rootFolder : '.x-root-folder',
seasonFolder : '.x-season-folder',
seriesType : '.x-series-type',
monitor : '.x-monitor',
monitorTooltip : '.x-monitor-tooltip',
addButton : '.x-add',
addAlbumButton : '.x-add-album',
addSearchButton : '.x-add-search',
addAlbumSearchButton : '.x-add-album-search',
overview : '.x-overview'
},
events : {
'click .x-add' : '_addWithoutSearch',
'click .x-add-album' : '_addWithoutSearch',
'click .x-add-search' : '_addAndSearch',
'click .x-add-album-search' : '_addAndSearch',
'change .x-profile' : '_profileChanged',
'change .x-root-folder' : '_rootFolderChanged',
'change .x-season-folder' : '_seasonFolderChanged',
'change .x-series-type' : '_seriesTypeChanged',
'change .x-monitor' : '_monitorChanged'
},
initialize : function() {
if (!this.model) {
throw 'model is required';
}
this.templateHelpers = {};
this._configureTemplateHelpers();
this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated);
this.listenTo(this.model, 'change', this.render);
this.listenTo(RootFolders, 'all', this._rootFoldersUpdated);
},
onRender : function() {
var defaultProfile = Config.getValue(Config.Keys.DefaultProfileId);
var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId);
var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true);
var defaultSeriesType = Config.getValue(Config.Keys.DefaultSeriesType, 'standard');
var defaultMonitorEpisodes = Config.getValue(Config.Keys.MonitorEpisodes, 'missing');
if (Profiles.get(defaultProfile)) {
this.ui.profile.val(defaultProfile);
}
if (RootFolders.get(defaultRoot)) {
this.ui.rootFolder.val(defaultRoot);
}
this.ui.seasonFolder.prop('checked', useSeasonFolder);
this.ui.seriesType.val(defaultSeriesType);
this.ui.monitor.val(defaultMonitorEpisodes);
//TODO: make this work via onRender, FM?
//works with onShow, but stops working after the first render
this.ui.overview.dotdotdot({
height : 120
});
this.templateFunction = Marionette.TemplateCache.get('AddSeries/MonitoringTooltipTemplate');
var content = this.templateFunction();
this.ui.monitorTooltip.popover({
content : content,
html : true,
trigger : 'hover',
title : 'Episode Monitoring Options',
placement : 'right',
container : this.$el
});
},
_configureTemplateHelpers : function() {
var existingSeries = ArtistCollection.where({ iTunesId : this.model.get('itunesId') });
if (existingSeries.length > 0) {
this.templateHelpers.existing = existingSeries[0].toJSON();
}
this.templateHelpers.profiles = Profiles.toJSON();
if (!this.model.get('isExisting')) {
this.templateHelpers.rootFolders = RootFolders.toJSON();
}
},
_onConfigUpdated : function(options) {
if (options.key === Config.Keys.DefaultProfileId) {
this.ui.profile.val(options.value);
}
else if (options.key === Config.Keys.DefaultRootFolderId) {
this.ui.rootFolder.val(options.value);
}
else if (options.key === Config.Keys.UseSeasonFolder) {
this.ui.seasonFolder.prop('checked', options.value);
}
else if (options.key === Config.Keys.DefaultSeriesType) {
this.ui.seriesType.val(options.value);
}
else if (options.key === Config.Keys.MonitorEpisodes) {
this.ui.monitor.val(options.value);
}
},
_profileChanged : function() {
Config.setValue(Config.Keys.DefaultProfileId, this.ui.profile.val());
},
_seasonFolderChanged : function() {
Config.setValue(Config.Keys.UseSeasonFolder, this.ui.seasonFolder.prop('checked'));
},
_rootFolderChanged : function() {
var rootFolderValue = this.ui.rootFolder.val();
if (rootFolderValue === 'addNew') {
var rootFolderLayout = new RootFolderLayout();
this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder);
AppLayout.modalRegion.show(rootFolderLayout);
} else {
Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue);
}
},
_seriesTypeChanged : function() {
Config.setValue(Config.Keys.DefaultSeriesType, this.ui.seriesType.val());
},
_monitorChanged : function() {
Config.setValue(Config.Keys.MonitorEpisodes, this.ui.monitor.val());
},
_setRootFolder : function(options) {
vent.trigger(vent.Commands.CloseModalCommand);
this.ui.rootFolder.val(options.model.id);
this._rootFolderChanged();
},
_addWithoutSearch : function(evt) {
console.log(evt);
this._addSeries(false);
},
_addAndSearch : function() {
this._addSeries(true);
},
_addSeries : function(searchForMissing) {
// TODO: Refactor to handle multiple add buttons/albums
var addButton = this.ui.addButton;
var addSearchButton = this.ui.addSearchButton;
console.log('_addSeries, searchForMissing=', searchForMissing);
addButton.addClass('disabled');
addSearchButton.addClass('disabled');
var profile = this.ui.profile.val();
var rootFolderPath = this.ui.rootFolder.children(':selected').text();
var seriesType = this.ui.seriesType.val(); // Perhaps make this a differnitator between artist or Album?
var seasonFolder = this.ui.seasonFolder.prop('checked');
var options = this._getAddSeriesOptions();
options.searchForMissing = searchForMissing;
this.model.set({
profileId : profile,
rootFolderPath : rootFolderPath,
seasonFolder : seasonFolder,
seriesType : seriesType,
addOptions : options,
monitored : true
}, { silent : true });
var self = this;
var promise = this.model.save();
if (searchForMissing) {
this.ui.addSearchButton.spinForPromise(promise);
}
else {
this.ui.addButton.spinForPromise(promise);
}
promise.always(function() {
addButton.removeClass('disabled');
addSearchButton.removeClass('disabled');
});
promise.done(function() {
console.log('[SearchResultView] _addSeries promise resolve:', self.model);
ArtistCollection.add(self.model);
self.close();
Messenger.show({
message : 'Added: ' + self.model.get('artistName'),
actions : {
goToSeries : {
label : 'Go to Artist',
action : function() {
Backbone.history.navigate('/artist/' + self.model.get('artistSlug'), { trigger : true });
}
}
},
hideAfter : 8,
hideOnNavigate : true
});
vent.trigger(vent.Events.SeriesAdded, { series : self.model });
});
},
_rootFoldersUpdated : function() {
this._configureTemplateHelpers();
this.render();
},
_getAddSeriesOptions : function() {
var monitor = this.ui.monitor.val();
//[TODO]: Refactor for albums
var lastSeason = _.max(this.model.get('seasons'), 'seasonNumber');
var firstSeason = _.min(_.reject(this.model.get('seasons'), { seasonNumber : 0 }), 'seasonNumber');
//this.model.setSeasonPass(firstSeason.seasonNumber); // TODO
var options = {
ignoreEpisodesWithFiles : false,
ignoreEpisodesWithoutFiles : false
};
if (monitor === 'all') {
return options;
}
else if (monitor === 'future') {
options.ignoreEpisodesWithFiles = true;
options.ignoreEpisodesWithoutFiles = true;
}
/*else if (monitor === 'latest') {
this.model.setSeasonPass(lastSeason.seasonNumber);
}
else if (monitor === 'first') {
this.model.setSeasonPass(lastSeason.seasonNumber + 1);
this.model.setSeasonMonitored(firstSeason.seasonNumber);
}*/
else if (monitor === 'missing') {
options.ignoreEpisodesWithFiles = true;
}
else if (monitor === 'existing') {
options.ignoreEpisodesWithoutFiles = true;
}
/*else if (monitor === 'none') {
this.model.setSeasonPass(lastSeason.seasonNumber + 1);
}*/
return options;
}
});
AsValidatedView.apply(view);
module.exports = view;

@ -1,146 +0,0 @@
<div class="search-item {{#unless isExisting}}search-item-new{{/unless}}">
<div class="row">
<div class="col-md-10">
<div class="row">
<div class="col-md-12">
<h2 class="series-title">
<!--{{titleWithYear}}-->
{{artistName}}
<!--<span class="labels">
<span class="label label-default">{{network}}</span>
{{#unless_eq status compare="continuing"}}
<span class="label label-danger">Ended</span>
{{/unless_eq}}
</span>-->
</h2>
</div>
</div>
<!-- <div class="row new-series-overview x-overview">
<div class="col-md-12 overview-internal">
{{overview}}
</div>
</div> -->
<div class="row">
{{#unless existing}}
{{#unless path}}
<div class="form-group col-md-4">
<label>Path</label>
{{> RootFolderSelectionPartial rootFolders}}
</div>
{{/unless}}
<div class="form-group col-md-2">
<label>Monitor <i class="icon-lidarr-form-info monitor-tooltip x-monitor-tooltip"></i></label>
<select class="form-control col-md-2 x-monitor">
<option value="all">All</option>
<option value="future">Future</option>
<option value="missing">Missing</option>
<option value="existing">Existing</option>
<option value="none">None</option>
</select>
</div>
<div class="form-group col-md-2">
<label>Profile</label>
{{> ProfileSelectionPartial profiles}}
</div>
<!--<div class="form-group col-md-2">
</div>-->
<div class="form-group col-md-2">
<label>Album Folders</label>
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" class="x-season-folder"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
</div>
</div>
{{/unless}}
</div>
<div class="row">
{{#unless existing}}
{{#if artistName}}
<div class="form-group col-md-2 col-md-offset-10">
<!--Uncomment if we need to add even more controls to add series-->
<!--<label style="visibility: hidden">Add</label>-->
<div class="btn-group">
<button class="btn btn-success add x-add" title="Add" data-artist="{{artistName}}">
<i class="icon-lidarr-add"></i>
</button>
<button class="btn btn-success add x-add-search" title="Add and Search for missing tracks" data-artist="{{artistName}}">
<i class="icon-lidarr-search"></i>
</button>
</div>
</div>
{{else}}
<div class="col-md-2 col-md-offset-10">
<button class="btn add-series disabled">
Add
</button>
</div>
{{/if}}
{{else}}
<div class="col-md-2 col-md-offset-10">
<a class="btn btn-default" href="{{route}}">
Already Exists
</a>
</div>
{{/unless}}
</div>
</div>
</div>
<div class="row">
{{#each albums}}
<div class="col-md-12" style="border:1px dashed black;">
<div class="col-md-2">
<a href="{{artworkUrl}}" target="_blank">
<!-- {{poster}} -->
<img class="album-poster" src="{{artworkUrl}}">
</a>
</div>
<div class="col-md-8">
<h2>{{albumName}} ({{year}})</h2>
{{#unless existing}}
{{#if albumName}}
<div class="form-group col-md-offset-10">
<!--Uncomment if we need to add even more controls to add series-->
<!--<label style="visibility: hidden">Add</label>-->
<div class="btn-group">
<button class="btn btn-success add x-add-album" title="Add" data-album="{{albumName}}">
<i class="icon-lidarr-add"></i>
</button>
<button class="btn btn-success add x-add-album-search" title="Add and Search for missing tracks" data-album="{{albumName}}">
<i class="icon-lidarr-search"></i>
</button>
</div>
</div>
{{else}}
<div class="col-md-2 col-md-offset-10">
<button class="btn add-series disabled">
Add
</button>
</div>
{{/if}}
{{else}}
<div class="col-md-2 col-md-offset-10">
<a class="btn btn-default" href="{{route}}">
Already Exists
</a>
</div>
{{/unless}}
</div>
</div>
{{/each}}
</div>
</div>

@ -1,5 +0,0 @@
<select class="form-control col-md-2 x-series-type" name="seriesType">
<option value="standard">Standard</option>
<option value="daily">Daily</option>
<option value="anime">Anime</option>
</select>

@ -1,13 +0,0 @@
<select class="form-control col-md-2 starting-season x-starting-season">
{{#each this}}
{{#if_eq seasonNumber compare="0"}}
<option value="{{seasonNumber}}">Specials</option>
{{else}}
<option value="{{seasonNumber}}">Season {{seasonNumber}}</option>
{{/if_eq}}
{{/each}}
<option value="5000000">None</option>
</select>

@ -1,181 +0,0 @@
@import "../Shared/Styles/card.less";
@import "../Shared/Styles/clickable.less";
#add-series-screen {
.existing-series {
.card();
margin : 30px 0px;
.unmapped-folder-path {
padding: 20px;
margin-left : 0px;
font-weight : 100;
font-size : 25px;
text-align : center;
}
.new-series-loadmore {
font-size : 30px;
font-weight : 300;
padding-top : 10px;
padding-bottom : 10px;
}
}
.new-series {
.search-item {
.card();
margin : 40px 0px;
}
}
.add-series-search {
margin-top : 20px;
margin-bottom : 20px;
}
.search-item {
padding-bottom : 20px;
.series-title {
margin-top : 5px;
.labels {
margin-left : 10px;
.label {
font-size : 12px;
vertical-align : middle;
}
}
.year {
font-style : italic;
color : #aaaaaa;
}
}
.new-series-overview {
overflow : hidden;
height : 103px;
.overview-internal {
overflow : hidden;
height : 80px;
}
}
.series-poster {
min-width : 138px;
min-height : 203px;
max-width : 138px;
max-height : 203px;
margin : 10px;
}
.album-poster {
min-width : 100px;
min-height : 100px;
max-width : 138px;
max-height : 203px;
margin : 10px;
}
a {
color : #343434;
}
a:hover {
text-decoration : none;
}
select {
font-size : 14px;
}
.checkbox {
margin-top : 0px;
}
.add {
i {
&:before {
color : #ffffff;
}
}
}
.monitor-tooltip {
margin-left : 5px;
}
}
.loading-folders {
margin : 30px 0px;
text-align: center;
}
.hint {
color : #999999;
font-style : italic;
}
.monitor-tooltip-contents {
padding-bottom : 0px;
dd {
padding-bottom : 8px;
}
}
}
li.add-new {
.clickable;
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 20px;
color: rgb(51, 51, 51);
white-space: nowrap;
}
li.add-new:hover {
text-decoration: none;
color: rgb(255, 255, 255);
background-color: rgb(0, 129, 194);
}
.root-folders-modal {
overflow : visible;
.root-folders-list {
overflow-y : auto;
max-height : 300px;
i {
.clickable();
}
}
.validation-errors {
display : none;
}
.input-group {
.form-control {
background-color : white;
}
}
.root-folders {
margin-top : 20px;
}
.recent-folder {
.clickable();
}
}

@ -2,9 +2,27 @@ var Backbone = require('backbone');
var AlbumModel = require('./AlbumModel');
module.exports = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/album',
model : AlbumModel,
comparator : function(season) {
return -season.get('seasonNumber');
originalFetch : Backbone.Collection.prototype.fetch,
initialize : function(options) {
this.artistId = options.artistId;
this.models = [];
},
fetch : function(options) {
if (!this.artistId) {
throw 'artistId is required';
}
if (!options) {
options = {};
}
options.data = { artistId : this.artistId };
return this.originalFetch.call(this, options);
}
});

@ -1,11 +1,8 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({
defaults : {
seasonNumber : 0
artistId : 0
},
initialize : function() {
this.set('id', this.get('seasonNumber'));
}
});

@ -67,7 +67,7 @@ var Collection = PageableCollection.extend({
'missing' : [
null,
null,
function(model) { return model.get('episodeCount') !== model.get('episodeFileCount'); }
function(model) { return model.get('trackCount') !== model.get('trackFileCount'); }
]
},
@ -96,12 +96,12 @@ var Collection = PageableCollection.extend({
}
},
percentOfEpisodes : {
percentOfTracks : {
sortValue : function(model, attr) {
var percentOfEpisodes = model.get(attr);
var episodeCount = model.get('episodeCount');
var percentOfTracks = model.get(attr);
var trackCount = model.get('trackCount');
return percentOfEpisodes + episodeCount / 1000000;
return percentOfTracks + trackCount / 1000000;
}
},

@ -5,14 +5,15 @@ module.exports = Backbone.Model.extend({
urlRoot : window.NzbDrone.ApiRoot + '/artist',
defaults : {
episodeFileCount : 0,
episodeCount : 0,
trackFileCount : 0,
trackCount : 0,
isExisting : false,
status : 0
},
setAlbumsMonitored : function(albumName) {
_.each(this.get('albums'), function(album) {
console.log(album);
if (album.albumName === albumName) {
album.monitored = !album.monitored;
}

@ -1,23 +1,25 @@
var _ = require('underscore');
var Marionette = require('marionette');
var SeasonLayout = require('./AlbumLayout');
var AlbumLayout = require('./AlbumLayout');
var AsSortedCollectionView = require('../../Mixins/AsSortedCollectionView');
var view = Marionette.CollectionView.extend({
itemView : SeasonLayout,
itemView : AlbumLayout,
initialize : function(options) {
if (!options.trackCollection) {
throw 'trackCollection is needed';
}
console.log(options);
this.albumCollection = options.collection;
this.trackCollection = options.trackCollection;
this.artist = options.artist;
},
itemViewOptions : function() {
return {
albumCollection : this.albumCollection,
trackCollection : this.trackCollection,
artist : this.artist
};

@ -0,0 +1,11 @@
var Marionette = require('marionette');
module.exports = Marionette.ItemView.extend({
template : 'Artist/Details/AlbumInfoViewTemplate',
initialize : function(options) {
this.listenTo(this.model, 'change', this.render);
},
});

@ -0,0 +1,41 @@
<div class="row">
<div class="col-md-9">
{{profile profileId}}
{{#if label}}
<span class="label label-info">{{label}}</span>
{{/if}}
<span class="label label-info">{{path}}</span>
{{#if ratings}}
<span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span>
{{/if}}
</div>
<div class="col-md-9">
<span class="album-info-links">
<a href="{{MBAlbumUrl}}" class="label label-info">MusicBrainz</a>
{{#if tadbId}}
<a href="{{TADBAlbumUrl}}" class="label label-info">The AudioDB</a>
{{/if}}
{{#if discogsId}}
<a href="{{discogsAlbumUrl}}" class="label label-info">Discogs</a>
{{/if}}
{{#if amId}}
<a href="{{allMusicAlbumUrl}}" class="label label-info">AllMusic</a>
{{/if}}
</span>
</div>
</div>
{{#if tags}}
<div class="row">
<div class="col-md-12">
{{tagDisplay tags}}
</div>
</div>
{{/if}}

@ -1,39 +1,44 @@
var vent = require('vent');
var Marionette = require('marionette');
var Backgrid = require('backgrid');
var ToggleCell = require('../../Cells/EpisodeMonitoredCell');
var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell');
var ToggleCell = require('../../Cells/TrackMonitoredCell');
var TrackTitleCell = require('../../Cells/TrackTitleCell');
var TrackExplicitCell = require('../../Cells/TrackExplicitCell');
var RelativeDateCell = require('../../Cells/RelativeDateCell');
var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell');
var EpisodeActionsCell = require('../../Cells/EpisodeActionsCell');
var TrackStatusCell = require('../../Cells/TrackStatusCell');
var TrackActionsCell = require('../../Cells/TrackActionsCell');
var TrackNumberCell = require('./TrackNumberCell');
var TrackWarningCell = require('./TrackWarningCell');
var TrackRatingCell = require('./TrackRatingCell');
var AlbumInfoView = require('./AlbumInfoView');
var CommandController = require('../../Commands/CommandController');
var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout');
//var TrackFileEditorLayout = require('../../TrackFile/Editor/TrackFileEditorLayout');
var moment = require('moment');
var _ = require('underscore');
var Messenger = require('../../Shared/Messenger');
module.exports = Marionette.Layout.extend({
template : 'Artist/Details/ArtistLayoutTemplate',
template : 'Artist/Details/AlbumLayoutTemplate',
ui : {
seasonSearch : '.x-album-search',
seasonMonitored : '.x-album-monitored',
seasonRename : '.x-album-rename'
albumSearch : '.x-album-search',
albumMonitored : '.x-album-monitored',
albumRename : '.x-album-rename',
cover : '.x-album-cover'
},
events : {
'click .x-album-episode-file-editor' : '_openEpisodeFileEditor',
'click .x-track-file-editor' : '_openTrackFileEditor',
'click .x-album-monitored' : '_albumMonitored',
'click .x-album-search' : '_albumSearch',
'click .x-album-rename' : '_albumRename',
'click .x-show-hide-episodes' : '_showHideEpisodes',
'dblclick .artist-album h2' : '_showHideEpisodes'
'click .x-show-hide-tracks' : '_showHideTracks',
'dblclick .artist-album h2' : '_showHideTracks'
},
regions : {
episodeGrid : '.x-episode-grid'
trackGrid : '.x-track-grid',
albumInfo : '#album-info'
},
columns : [
@ -61,110 +66,135 @@ module.exports = Marionette.Layout.extend({
{
name : 'this',
label : 'Title',
hideSeriesLink : true,
cell : EpisodeTitleCell,
hideArtistLink : true,
cell : TrackTitleCell,
sortable : false
},
{
name : 'airDateUtc',
label : 'Air Date',
cell : RelativeDateCell
name : 'this',
label : 'Rating',
cell : TrackRatingCell
},
{
name : 'status',
label : 'Status',
cell : EpisodeStatusCell,
sortable : false
name : 'this',
label : 'Content',
cell : TrackExplicitCell
},
{
name : 'this',
label : '',
cell : EpisodeActionsCell,
sortable : false
}
//{
// name : 'airDateUtc',
// label : 'Air Date',
// cell : RelativeDateCell
//},
{
name : 'status',
label : 'Status',
cell : TrackStatusCell,
sortable : false
},
//{
// name : 'this',
// label : '',
// cell : TrackActionsCell,
// sortable : false
//}
],
templateHelpers : function() {
var episodeCount = this.episodeCollection.filter(function(episode) {
return episode.get('hasFile') || episode.get('monitored') && moment(episode.get('airDateUtc')).isBefore(moment());
var trackCount = this.trackCollection.filter(function(track) {
return track.get('hasFile') || track.get('monitored');
}).length;
var episodeFileCount = this.episodeCollection.where({ hasFile : true }).length;
var percentOfEpisodes = 100;
var trackFileCount = this.trackCollection.where({ hasFile : true }).length;
var percentOfTracks = 100;
if (episodeCount > 0) {
percentOfEpisodes = episodeFileCount / episodeCount * 100;
if (trackCount > 0) {
percentOfTracks = trackFileCount / trackCount * 100;
}
return {
showingEpisodes : this.showingEpisodes,
episodeCount : episodeCount,
episodeFileCount : episodeFileCount,
percentOfEpisodes : percentOfEpisodes
showingTracks : this.showingTracks,
trackCount : trackCount,
trackFileCount : trackFileCount,
percentOfTracks : percentOfTracks
};
},
initialize : function(options) {
if (!options.episodeCollection) {
throw 'episodeCollection is required';
if (!options.trackCollection) {
throw 'trackCollection is required';
}
this.artist = options.artist;
this.fullTrackCollection = options.trackCollection;
this.trackCollection = this.fullTrackCollection.byAlbum(this.model.get('id'));
this._updateTrackCollection();
this.series = options.series;
this.fullEpisodeCollection = options.episodeCollection;
this.episodeCollection = this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber'));
this._updateEpisodeCollection();
console.log(options);
this.showingEpisodes = this._shouldShowEpisodes();
this.showingTracks = this._shouldShowTracks();
this.listenTo(this.model, 'sync', this._afterSeasonMonitored);
this.listenTo(this.episodeCollection, 'sync', this.render);
this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpisodes);
this.listenTo(this.trackCollection, 'sync', this.render);
this.listenTo(this.fullTrackCollection, 'sync', this._refreshTracks);
this.listenTo(this.model, 'change:images', this._updateImages);
},
onRender : function() {
if (this.showingEpisodes) {
this._showEpisodes();
if (this.showingTracks) {
this._showTracks();
}
this._setSeasonMonitoredState();
this._showAlbumInfo();
this._setAlbumMonitoredState();
CommandController.bindToCommand({
element : this.ui.seasonSearch,
element : this.ui.albumSearch,
command : {
name : 'seasonSearch',
seriesId : this.series.id,
seasonNumber : this.model.get('seasonNumber')
name : 'albumSearch',
artistId : this.artist.id,
albumId : this.model.get('albumId')
}
});
CommandController.bindToCommand({
element : this.ui.seasonRename,
element : this.ui.albumRename,
command : {
name : 'renameFiles',
seriesId : this.series.id,
seasonNumber : this.model.get('seasonNumber')
artistId : this.artist.id,
albumId : this.model.get('albumId')
}
});
},
_seasonSearch : function() {
CommandController.Execute('seasonSearch', {
name : 'seasonSearch',
seriesId : this.series.id,
seasonNumber : this.model.get('seasonNumber')
_getImage : function(type) {
var image = _.where(this.model.get('images'), { coverType : type });
if (image && image[0]) {
return image[0].url;
}
return undefined;
},
_albumSearch : function() {
CommandController.Execute('albumSearch', {
name : 'albumSearch',
artistId : this.artist.id,
albumId : this.model.get('albumId')
});
},
_seasonRename : function() {
_albumRename : function() {
vent.trigger(vent.Commands.ShowRenamePreview, {
series : this.series,
seasonNumber : this.model.get('seasonNumber')
artist : this.artist,
albumId : this.model.get('albumId')
});
},
_seasonMonitored : function() {
if (!this.series.get('monitored')) {
_albumMonitored : function() {
if (!this.artist.get('monitored')) {
Messenger.show({
message : 'Unable to change monitored state when series is not monitored',
@ -176,80 +206,86 @@ module.exports = Marionette.Layout.extend({
var name = 'monitored';
this.model.set(name, !this.model.get(name));
this.series.setSeasonMonitored(this.model.get('seasonNumber'));
this.artist.setSeasonMonitored(this.model.get('albumId'));
var savePromise = this.series.save().always(this._afterSeasonMonitored.bind(this));
var savePromise = this.artist.save().always(this._afterSeasonMonitored.bind(this));
this.ui.seasonMonitored.spinForPromise(savePromise);
this.ui.albumMonitored.spinForPromise(savePromise);
},
_afterSeasonMonitored : function() {
var self = this;
_.each(this.episodeCollection.models, function(episode) {
episode.set({ monitored : self.model.get('monitored') });
_.each(this.trackCollection.models, function(track) {
track.set({ monitored : self.model.get('monitored') });
});
this.render();
},
_setSeasonMonitoredState : function() {
this.ui.seasonMonitored.removeClass('icon-lidarr-spinner fa-spin');
_setAlbumMonitoredState : function() {
this.ui.albumMonitored.removeClass('icon-lidarr-spinner fa-spin');
if (this.model.get('monitored')) {
this.ui.seasonMonitored.addClass('icon-lidarr-monitored');
this.ui.seasonMonitored.removeClass('icon-lidarr-unmonitored');
this.ui.albumMonitored.addClass('icon-lidarr-monitored');
this.ui.albumMonitored.removeClass('icon-lidarr-unmonitored');
} else {
this.ui.seasonMonitored.addClass('icon-lidarr-unmonitored');
this.ui.seasonMonitored.removeClass('icon-lidarr-monitored');
this.ui.albumMonitored.addClass('icon-lidarr-unmonitored');
this.ui.albumMonitored.removeClass('icon-lidarr-monitored');
}
},
_showEpisodes : function() {
this.episodeGrid.show(new Backgrid.Grid({
_showTracks : function() {
this.trackGrid.show(new Backgrid.Grid({
columns : this.columns,
collection : this.episodeCollection,
className : 'table table-hover season-grid'
collection : this.trackCollection,
className : 'table table-hover track-grid'
}));
},
_showAlbumInfo : function() {
this.albumInfo.show(new AlbumInfoView({
model : this.model
}));
},
_shouldShowEpisodes : function() {
_shouldShowTracks : function() {
var startDate = moment().add('month', -1);
var endDate = moment().add('year', 1);
return true;
//return this.trackCollection.some(function(track) {
// var airDate = track.get('releasedDate');
return this.episodeCollection.some(function(episode) {
var airDate = episode.get('airDateUtc');
// if (airDate) {
// var airDateMoment = moment(airDate);
if (airDate) {
var airDateMoment = moment(airDate);
// if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) {
// return true;
// }
// }
if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) {
return true;
}
}
return false;
});
// return false;
//});
},
_showHideEpisodes : function() {
if (this.showingEpisodes) {
this.showingEpisodes = false;
this.episodeGrid.close();
_showHideTracks : function() {
if (this.showingTracks) {
this.showingTracks = false;
this.trackGrid.close();
} else {
this.showingEpisodes = true;
this._showEpisodes();
this.showingTracks = true;
this._showTracks();
}
this.templateHelpers.showingEpisodes = this.showingEpisodes;
this.templateHelpers.showingTracks = this.showingTracks;
this.render();
},
_episodeMonitoredToggled : function(options) {
_trackMonitoredToggled : function(options) {
var model = options.model;
var shiftKey = options.shiftKey;
if (!this.episodeCollection.get(model.get('id'))) {
if (!this.trackCollection.get(model.get('id'))) {
return;
}
@ -257,45 +293,53 @@ module.exports = Marionette.Layout.extend({
return;
}
var lastToggled = this.episodeCollection.lastToggled;
var lastToggled = this.trackCollection.lastToggled;
if (!lastToggled) {
return;
}
var currentIndex = this.episodeCollection.indexOf(model);
var lastIndex = this.episodeCollection.indexOf(lastToggled);
var currentIndex = this.trackCollection.indexOf(model);
var lastIndex = this.trackCollection.indexOf(lastToggled);
var low = Math.min(currentIndex, lastIndex);
var high = Math.max(currentIndex, lastIndex);
var range = _.range(low + 1, high);
this.episodeCollection.lastToggled = model;
this.trackCollection.lastToggled = model;
},
_updateEpisodeCollection : function() {
_updateTrackCollection : function() {
var self = this;
this.episodeCollection.add(this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')).models, { merge : true });
this.trackCollection.add(this.fullTrackCollection.byAlbum(this.model.get('albumId')).models, { merge : true });
this.episodeCollection.each(function(model) {
model.episodeCollection = self.episodeCollection;
this.trackCollection.each(function(model) {
model.trackCollection = self.trackCollection;
});
},
_refreshEpisodes : function() {
this._updateEpisodeCollection();
this.episodeCollection.fullCollection.sort();
_updateImages : function () {
var cover = this._getImage('cover');
if (cover) {
this.ui.poster.attr('src', cover);
}
},
_refreshTracks : function() {
this._updateTrackCollection();
this.trackCollection.fullCollection.sort();
this.render();
},
_openEpisodeFileEditor : function() {
var view = new EpisodeFileEditorLayout({
model : this.model,
series : this.series,
episodeCollection : this.episodeCollection
});
_openTrackFileEditor : function() {
//var view = new TrackFileEditorLayout({
// model : this.model,
// artist : this.artist,
// trackCollection : this.trackCollection
//});
vent.trigger(vent.Commands.OpenModalCommand, view);
//vent.trigger(vent.Commands.OpenModalCommand, view);
}
});

@ -1,50 +1,68 @@
<div class="artist-album" id="season-{{seasonNumber}}">
<h2>
<i class="x-album-monitored album-monitored clickable" title="Toggle album monitored status"/>
{{#if seasonNumber}}
Season {{seasonNumber}}
{{else}}
Specials
{{/if}}
<div class="artist-album" id="album-{{title}}">
<div class="visible-lg col-lg-2 poster">
{{cover}}
</div>
<div class="col-md-12 col-lg-10">
<h2 class="header-text">
<i class="x-album-monitored album-monitored clickable" title="Toggle album monitored status"/>
{{#if title}}
{{title}} ({{albumYear}})
{{#if_eq episodeCount compare=0}}
{{#if monitored}}
<span class="badge badge-primary album-status" title="No aired episodes">&nbsp;</span>
{{else}}
<span class="badge badge-warning album-status" title="Album is not monitored">&nbsp;</span>
Specials
{{/if}}
{{else}}
{{#if_eq percentOfEpisodes compare=100}}
<span class="badge badge-success album-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span>
{{#if_eq trackCount compare=0}}
{{#if monitored}}
<span class="badge badge-primary album-status" title="No aired tracks">&nbsp;</span>
{{else}}
<span class="badge badge-warning album-status" title="Album is not monitored">&nbsp;</span>
{{/if}}
{{else}}
<span class="badge badge-danger album-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span>
{{#if_eq percentOfTracks compare=100}}
<span class="badge badge-success album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}} / {{trackCount}} Tracks</span>
{{else}}
<span class="badge badge-danger album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}} / {{trackCount}} Tracks</span>
{{/if_eq}}
{{/if_eq}}
{{/if_eq}}
<span class="album-actions pull-right">
<div class="x-album-episode-file-editor">
<i class="icon-lidarr-episode-file" title="Modify episode files for album"/>
</div>
<div class="x-album-rename">
<i class="icon-lidarr-rename" title="Preview rename for album {{seasonNumber}}"/>
</div>
<div class="x-album-search">
<i class="icon-lidarr-search" title="Search for monitored episodes in album {{seasonNumber}}"/>
</div>
</span>
</h2>
<div class="show-hide-episodes x-show-hide-episodes">
<h4>
{{#if showingEpisodes}}
<i class="icon-lidarr-panel-hide"/>
Hide Episodes
{{else}}
<i class="icon-lidarr-panel-show"/>
Show Episodes
{{/if}}
</h4>
<span class="album-actions pull-right">
<div class="x-track-file-editor">
<i class="icon-lidarr-track-file" title="Modify track files for album"/>
</div>
<div class="x-album-rename">
<i class="icon-lidarr-rename" title="Preview rename for album {{albumId}}"/>
</div>
<div class="x-album-search">
<i class="icon-lidarr-search" title="Search for monitored tracks in album {{albumId}}"/>
</div>
</span>
</h2>
<div class="album-detail-release">
Release Date: {{albumReleaseDate}}
</div>
<div class="album-detail-type">
Type: Album
</div>
<div id="album-info" class="album-info"></div>
</div>
<div class="x-episode-grid table-responsive"></div>
<div class="show-hide-tracks x-show-hide-tracks">
<h4>
{{#if showingTracks}}
<i class="icon-lidarr-panel-hide"/>
Hide Tracks
{{else}}
<i class="icon-lidarr-panel-show"/>
Show Tracks
{{/if}}
</h4>
</div>
<div class="x-track-grid table-responsive"></div>
</div>

@ -36,7 +36,7 @@ module.exports = Marionette.Layout.extend({
},
events : {
'click .x-episode-file-editor' : '_openEpisodeFileEditor',
'click .x-track-file-editor' : '_openTrackFileEditor',
'click .x-monitored' : '_toggleMonitored',
'click .x-edit' : '_editArtist',
'click .x-refresh' : '_refreshArtist',
@ -142,7 +142,7 @@ module.exports = Marionette.Layout.extend({
_refreshArtist : function() {
CommandController.Execute('refreshArtist', {
name : 'refreshArtist',
seriesId : this.model.id
artistId : this.model.id
});
},
@ -167,30 +167,19 @@ module.exports = Marionette.Layout.extend({
this.albums.show(new LoadingView());
this.albumCollection = new AlbumCollection(this.model.get('albums'));
this.albumCollection = new AlbumCollection({ artistId : this.model.id }).bindSignalR();
this.trackCollection = new TrackCollection({ artistId : this.model.id }).bindSignalR();
this.trackFileCollection = new TrackFileCollection({ artistId : this.model.id }).bindSignalR();
console.log (this.trackCollection);
reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(trackFileId) {
return self.trackFileCollection.get(trackFileId);
});
reqres.setHandler(reqres.Requests.GetAlternateNameBySeasonNumber, function(artistId, seasonNumber, sceneSeasonNumber) {
if (self.model.get('id') !== artistId) {
return [];
}
if (sceneSeasonNumber === undefined) {
sceneSeasonNumber = seasonNumber;
}
return _.where(self.model.get('alternateTitles'),
function(alt) {
return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber;
});
});
$.when(this.trackCollection.fetch(), this.trackFileCollection.fetch()).done(function() {
$.when(this.albumCollection.fetch(), this.trackCollection.fetch(), this.trackFileCollection.fetch()).done(function() {
var albumCollectionView = new AlbumCollectionView({
collection : self.albumCollection,
trackCollection : self.trackCollection,
@ -219,7 +208,7 @@ module.exports = Marionette.Layout.extend({
},
_refresh : function() {
this.albumCollection.add(this.model.get('albums'), { merge : true });
this.albumCollection.fetch();
this.trackCollection.fetch();
this.trackFileCollection.fetch();

@ -9,7 +9,7 @@
{{name}}
<div class="artist-actions pull-right">
<div class="x-episode-file-editor">
<i class="icon-lidarr-episode-file" title="Modify episode files for artist"/>
<i class="icon-lidarr-track-file" title="Modify episode files for artist"/>
</div>
<div class="x-refresh">
<i class="icon-lidarr-refresh icon-can-spin" title="Update artist info and scan disk"/>
@ -32,4 +32,4 @@
<div id="info" class="artist-info"></div>
</div>
</div>
<div id="seasons"></div>
<div id="albums"></div>

@ -39,7 +39,7 @@
<a href="{{discogsUrl}}" class="label label-info">Discogs</a>
{{/if}}
{{#if amId}}
{{#if allMusicId}}
<a href="{{allMusicUrl}}" class="label label-info">AllMusic</a>
{{/if}}
</span>

@ -4,18 +4,14 @@ var reqres = require('../../reqres');
var ArtistCollection = require('../ArtistCollection');
module.exports = NzbDroneCell.extend({
className : 'episode-number-cell',
className : 'track-number-cell',
template : 'Artist/Details/TrackNumberCellTemplate',
render : function() {
this.$el.empty();
this.$el.html(this.model.get('trackNumber'));
var series = ArtistCollection.get(this.model.get('seriesId'));
if (series.get('seriesType') === 'anime' && this.model.has('absoluteEpisodeNumber')) {
this.$el.html('{0} ({1})'.format(this.model.get('episodeNumber'), this.model.get('absoluteEpisodeNumber')));
}
var artist = ArtistCollection.get(this.model.get('artistId'));
var alternateTitles = [];

@ -0,0 +1,19 @@
var NzbDroneCell = require('../../Cells/NzbDroneCell');
var ArtistCollection = require('../ArtistCollection');
module.exports = NzbDroneCell.extend({
className : 'track-rating-cell',
render : function() {
this.$el.empty();
var ratings = this.model.get('ratings')
if (ratings) {
this.$el.html(ratings.value + ' (' + ratings.votes + ' votes)');
}
this.delegateEvents();
return this;
}
});

@ -9,7 +9,7 @@ var RelativeDateCell = require('../../Cells/RelativeDateCell');
var ArtistTitleCell = require('../../Cells/ArtistTitleCell');
var TemplatedCell = require('../../Cells/TemplatedCell');
var ProfileCell = require('../../Cells/ProfileCell');
var EpisodeProgressCell = require('../../Cells/EpisodeProgressCell');
var TrackProgressCell = require('../../Cells/TrackProgressCell');
var ArtistActionsCell = require('../../Cells/ArtistActionsCell');
var ArtistStatusCell = require('../../Cells/ArtistStatusCell');
var FooterView = require('./FooterView');
@ -62,10 +62,10 @@ module.exports = Marionette.Layout.extend({
cell : RelativeDateCell
},
{
name : 'percentOfEpisodes',
name : 'percentOfTracks',
label : 'Tracks',
cell : EpisodeProgressCell,
className : 'episode-progress-cell'
cell : TrackProgressCell,
className : 'track-progress-cell'
},
{
name : 'this',
@ -157,7 +157,7 @@ module.exports = Marionette.Layout.extend({
},
{
title : 'Tracks',
name : 'percentOfEpisodes'
name : 'percentOfTracks'
}
]
};
@ -330,8 +330,8 @@ module.exports = Marionette.Layout.extend({
_.each(this.artistCollection.models, function(model) {
albums += model.get('albumCount');
tracks += model.get('episodeCount'); // TODO: Refactor to Seasons and Tracks
trackFiles += model.get('episodeFileCount');
tracks += model.get('trackCount'); // TODO: Refactor to Seasons and Tracks
trackFiles += model.get('trackFileCount');
/*if (model.get('status').toLowerCase() === 'ended') {
ended++;

@ -48,7 +48,7 @@
{{profile profileId}}
</div>
<div class="col-md-2 col-xs-4">
{{> EpisodeProgressPartial }}
{{> TrackProgressPartial }}
</div>
<div class="col-md-8 col-xs-10">
Path {{path}}

@ -21,7 +21,7 @@ module.exports = PageableCollection.extend({
this.artistId = options.artistId;
},
bySeason : function(album) {
byAlbum : function(album) {
var filtered = this.filter(function(track) {
return track.get('albumId') === album;
});
@ -36,11 +36,11 @@ module.exports = PageableCollection.extend({
var track2 = model2.get('trackNumber');
if (track1 < track2) {
return 1;
return -1;
}
if (track1 > track2) {
return -1;
return 1;
}
return 0;

@ -8,6 +8,11 @@
max-width: 100%;
}
.album-cover {
min-width: 56px;
max-width: 100%;
}
.truncate {
white-space: nowrap;
overflow: hidden;
@ -22,6 +27,11 @@
width : 168px;
}
.album-cover {
padding-left : 20px;
width : 168px;
}
.form-horizontal {
margin-top : 10px;
}
@ -81,9 +91,9 @@
.card;
.opacity(0.9);
margin : 30px 10px;
padding : 10px 25px;
padding : 30px 25px;
.show-hide-episodes {
.show-hide-tracks {
.clickable();
text-align : center;
@ -91,6 +101,9 @@
.clickable();
}
}
.header-text {
margin-top : 0px;
}
}
.artist-posters {
@ -261,39 +274,45 @@
.artist-album {
.episode-number-cell {
.track-number-cell {
width : 40px;
white-space: nowrap;
}
.episode-air-date-cell {
.track-air-date-cell {
width : 150px;
}
.episode-status-cell {
.track-status-cell {
width : 100px;
}
.track-rating-cell {
width : 150px;
}
.track-explicit-cell {
width : 100px;
}
.episode-title-cell {
.track-title-cell {
cursor : pointer;
}
}
.episode-detail-modal {
.track-detail-modal {
.episode-info {
.track-info {
margin-bottom : 10px;
}
.episode-overview {
.track-overview {
font-style : italic;
}
.episode-file-info {
.track-file-info {
margin-top : 30px;
font-size : 12px;
}
.episode-history-details-cell .popover {
.track-history-details-cell .popover {
max-width: 800px;
}
@ -302,7 +321,7 @@
}
}
.album-grid {
.track-grid {
.toggle-cell {
width : 28px;
text-align : center;
@ -422,7 +441,7 @@
//artist Details
.artist-not-monitored {
.album-monitored, .episode-monitored {
.album-monitored, .track-monitored {
color: #888888;
cursor: not-allowed;
@ -454,6 +473,28 @@
}
}
.album-info {
.row {
margin-bottom : 3px;
.label {
display : inline-block;
margin-bottom : 2px;
padding : 4px 6px 3px 6px;
max-width : 100%;
white-space : normal;
word-wrap : break-word;
}
}
.album-info-links {
@media (max-width: @screen-sm-max) {
display : inline-block;
margin-top : 5px;
}
}
}
.scene-info {
.key, .value {
display : inline-block;

@ -1 +0,0 @@
{{> EpisodeProgressPartial }}

@ -0,0 +1,44 @@
var vent = require('vent');
var NzbDroneCell = require('./NzbDroneCell');
var CommandController = require('../Commands/CommandController');
module.exports = NzbDroneCell.extend({
className : 'track-actions-cell',
events : {
'click .x-automatic-search' : '_automaticSearch',
'click .x-manual-search' : '_manualSearch'
},
render : function() {
this.$el.empty();
this.$el.html('<i class="icon-lidarr-search x-automatic-search" title="Automatic Search"></i>' + '<i class="icon-lidarr-search-manual x-manual-search" title="Manual Search"></i>');
CommandController.bindToCommand({
element : this.$el.find('.x-automatic-search'),
command : {
name : 'trackSearch',
trackIds : [this.model.get('id')]
}
});
this.delegateEvents();
return this;
},
_automaticSearch : function() {
CommandController.Execute('trackSearch', {
name : 'trackSearch',
trackIds : [this.model.get('id')]
});
},
_manualSearch : function() {
vent.trigger(vent.Commands.ShowTrackDetails, {
track : this.cellValue,
hideSeriesLink : true,
openingTab : 'search'
});
}
});

@ -0,0 +1,20 @@
var vent = require('vent');
var NzbDroneCell = require('./NzbDroneCell');
module.exports = NzbDroneCell.extend({
className : 'track-explicit-cell',
template : 'Cells/TrackExplicitCellTemplate',
render : function() {
var explicit = this.cellValue.get('explicit');
var print = '';
if (explicit === true) {
print = 'Explicit';
}
this.$el.html(print);
return this;
}
});

@ -0,0 +1,57 @@
var _ = require('underscore');
var ToggleCell = require('./ToggleCell');
var ArtistCollection = require('../Artist/ArtistCollection');
var Messenger = require('../Shared/Messenger');
module.exports = ToggleCell.extend({
className : 'toggle-cell track-monitored',
_originalOnClick : ToggleCell.prototype._onClick,
_onClick : function(e) {
var artist = ArtistCollection.get(this.model.get('artistId'));
if (!artist.get('monitored')) {
Messenger.show({
message : 'Unable to change monitored state when series is not monitored',
type : 'error'
});
return;
}
if (e.shiftKey && this.model.trackCollection.lastToggled) {
this._selectRange();
return;
}
this._originalOnClick.apply(this, arguments);
this.model.trackCollection.lastToggled = this.model;
},
_selectRange : function() {
var trackCollection = this.model.trackCollection;
var lastToggled = trackCollection.lastToggled;
var currentIndex = trackCollection.indexOf(this.model);
var lastIndex = trackCollection.indexOf(lastToggled);
var low = Math.min(currentIndex, lastIndex);
var high = Math.max(currentIndex, lastIndex);
var range = _.range(low + 1, high);
_.each(range, function(index) {
var model = trackCollection.at(index);
model.set('monitored', lastToggled.get('monitored'));
model.save();
});
this.model.set('monitored', lastToggled.get('monitored'));
this.model.save();
this.model.trackCollection.lastToggled = undefined;
}
});

@ -2,21 +2,21 @@ var Marionette = require('marionette');
var NzbDroneCell = require('./NzbDroneCell');
module.exports = NzbDroneCell.extend({
className : 'episode-progress-cell',
template : 'Cells/EpisodeProgressCellTemplate',
className : 'track-progress-cell',
template : 'Cells/TrackProgressCellTemplate',
render : function() {
var episodeCount = this.model.get('episodeCount');
var episodeFileCount = this.model.get('episodeFileCount');
var trackCount = this.model.get('trackCount');
var trackFileCount = this.model.get('trackFileCount');
var percent = 100;
if (episodeCount > 0) {
percent = episodeFileCount / episodeCount * 100;
if (trackCount > 0) {
percent = trackFileCount / trackCount * 100;
}
this.model.set('percentOfEpisodes', percent);
this.model.set('percentOfTracks', percent);
this.templateFunction = Marionette.TemplateCache.get(this.template);
var data = this.model.toJSON();

@ -0,0 +1 @@
{{> TrackProgressPartial }}

@ -0,0 +1,127 @@
var reqres = require('../reqres');
var Backbone = require('backbone');
var NzbDroneCell = require('./NzbDroneCell');
var QueueCollection = require('../Activity/Queue/QueueCollection');
var moment = require('moment');
var FormatHelpers = require('../Shared/FormatHelpers');
module.exports = NzbDroneCell.extend({
className : 'track-status-cell',
render : function() {
this.listenTo(QueueCollection, 'sync', this._renderCell);
this._renderCell();
return this;
},
_renderCell : function() {
if (this.trackFile) {
this.stopListening(this.trackFile, 'change', this._refresh);
}
this.$el.empty();
if (this.model) {
var icon;
var tooltip;
var hasAired = moment(this.model.get('airDateUtc')).isBefore(moment());
this.trackFile = this._getFile();
if (this.trackFile) {
this.listenTo(this.trackFile, 'change', this._refresh);
var quality = this.trackFile.get('quality');
var revision = quality.revision;
var size = FormatHelpers.bytes(this.trackFile.get('size'));
var title = 'Track downloaded';
if (revision.real && revision.real > 0) {
title += '[REAL]';
}
if (revision.version && revision.version > 1) {
title += ' [PROPER]';
}
if (size !== '') {
title += ' - {0}'.format(size);
}
if (this.trackFile.get('qualityCutoffNotMet')) {
this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name));
} else {
this.$el.html('<span class="badge" title="{0}">{1}</span>'.format(title, quality.quality.name));
}
return;
}
else {
var model = this.model;
var downloading = false; //TODO Fix this by adding to QueueCollection
if (downloading) {
var progress = 100 - (downloading.get('sizeleft') / downloading.get('size') * 100);
if (progress === 0) {
icon = 'icon-lidarr-downloading';
tooltip = 'Track is downloading';
}
else {
this.$el.html('<div class="progress" title="Track is downloading - {0}% {1}">'.format(progress.toFixed(1), downloading.get('title')) +
'<div class="progress-bar progress-bar-purple" style="width: {0}%;"></div></div>'.format(progress));
return;
}
}
else if (this.model.get('grabbed')) {
icon = 'icon-lidarr-downloading';
tooltip = 'Track is downloading';
}
else if (!this.model.get('airDateUtc')) {
icon = 'icon-lidarr-tba';
tooltip = 'TBA';
}
else if (hasAired) {
icon = 'icon-lidarr-missing';
tooltip = 'Track missing from disk';
} else {
icon = 'icon-lidarr-not-aired';
tooltip = 'Track has not aired';
}
}
this.$el.html('<i class="{0}" title="{1}"/>'.format(icon, tooltip));
}
},
_getFile : function() {
var hasFile = this.model.get('hasFile');
if (hasFile) {
var trackFile;
if (reqres.hasHandler(reqres.Requests.GetTrackFileById)) {
trackFile = reqres.request(reqres.Requests.GetTrackFileById, this.model.get('trackFileId'));
}
else if (this.model.has('trackFile')) {
trackFile = new Backbone.Model(this.model.get('trackFile'));
}
if (trackFile) {
return trackFile;
}
}
return undefined;
}
});

@ -0,0 +1,29 @@
var vent = require('vent');
var NzbDroneCell = require('./NzbDroneCell');
module.exports = NzbDroneCell.extend({
className : 'track-title-cell',
events : {
'click' : '_showDetails'
},
render : function() {
var title = this.cellValue.get('title');
if (!title || title === '') {
title = 'TBA';
}
this.$el.html(title);
return this;
},
_showDetails : function() {
var hideArtistLink = this.column.get('hideArtistLink');
vent.trigger(vent.Commands.ShowTrackDetails, {
track : this.cellValue,
hideArtistLink : hideArtistLink
});
}
});

@ -15,7 +15,7 @@
}
}
.episode-title-cell {
.track-title-cell {
.text-overflow();
color: #428bca;
@ -89,7 +89,7 @@
}
}
td.episode-status-cell, td.quality-cell, td.history-quality-cell, td.progress-cell {
td.track-status-cell, td.quality-cell, td.history-quality-cell, td.progress-cell {
text-align: center;
width: 80px;
@ -118,7 +118,7 @@ td.episode-status-cell, td.quality-cell, td.history-quality-cell, td.progress-ce
word-wrap: break-word;
}
.episode-actions-cell {
.track-actions-cell {
width: 55px;
i {
@ -131,12 +131,12 @@ td.episode-status-cell, td.quality-cell, td.history-quality-cell, td.progress-ce
}
}
.episode-history-details-cell {
.track-history-details-cell {
width : 18px;
}
.episode-detail-modal {
.episode-actions-cell {
.track-detail-modal {
.track-actions-cell {
width : 18px;
}
}
@ -173,7 +173,7 @@ td.episode-status-cell, td.quality-cell, td.history-quality-cell, td.progress-ce
width : 80px;
}
td.delete-episode-file-cell {
td.delete-track-file-cell {
.clickable();
text-align : center;
@ -188,7 +188,7 @@ td.delete-episode-file-cell {
width: 16px;
}
.episode-number-cell {
.track-number-cell {
cursor : default;
}
@ -198,7 +198,7 @@ td.delete-episode-file-cell {
.table>tbody>tr>td, .table>thead>tr>th {
&.episode-warning-cell {
&.track-warning-cell {
width : 1px;
padding-left : 0px;
padding-right : 0px;

@ -496,7 +496,7 @@
.fa-icon-content(@fa-var-retweet);
}
.icon-lidarr-episode-file {
.icon-lidarr-track-file {
.fa-icon-content(@fa-var-file-video-o);
}

@ -2,7 +2,7 @@
@import "Bootstrap/variables";
@import "variables";
.progress.episode-progress {
.progress.track-progress {
position : relative;
margin-bottom : 2px;

@ -0,0 +1,46 @@
var Handlebars = require('handlebars');
var StatusModel = require('../../System/StatusModel');
var moment = require('moment');
var _ = require('underscore');
Handlebars.registerHelper('cover', function() {
var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png';
var cover = _.where(this.images, { coverType : 'cover' });
if (cover[0]) {
if (!cover[0].url.match(/^https?:\/\//)) {
return new Handlebars.SafeString('<img class="album-cover x-album-cover" {0}>'.format(Handlebars.helpers.defaultImg.call(null, cover[0].url, 250)));
} else {
var url = cover[0].url.replace(/^https?\:/, '');
return new Handlebars.SafeString('<img class="album-cover x-album-cover" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url)));
}
}
return new Handlebars.SafeString('<img class="album-cover placeholder-image" src="{0}">'.format(placeholder));
});
Handlebars.registerHelper('MBAlbumUrl', function() {
return 'https://musicbrainz.org/release-group/' + this.mbId;
});
Handlebars.registerHelper('TADBAlbumUrl', function() {
return 'http://www.theaudiodb.com/album/' + this.tadbId;
});
Handlebars.registerHelper('discogsAlbumUrl', function() {
return 'https://www.discogs.com/master/' + this.discogsId;
});
Handlebars.registerHelper('allMusicAlbumUrl', function() {
return 'http://www.allmusic.com/album/' + this.allMusicId;
});
Handlebars.registerHelper('albumYear', function() {
return new Handlebars.SafeString('<span class="year">{0}</span>'.format(moment(this.releaseDate).format('YYYY')));
});
Handlebars.registerHelper('albumReleaseDate', function() {
return new Handlebars.SafeString('<span class="release">{0}</span>'.format(moment(this.releaseDate).format('L')));
});

@ -9,16 +9,18 @@ Handlebars.registerHelper('poster', function() {
if (poster[0]) {
if (!poster[0].url.match(/^https?:\/\//)) {
return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster[0].url, 250)));
return new Handlebars.SafeString('<img class="artist-poster x-artist-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster[0].url, 250)));
} else {
var url = poster[0].url.replace(/^https?\:/, '');
return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url)));
return new Handlebars.SafeString('<img class="artist-poster x-artist-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url)));
}
}
return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder));
return new Handlebars.SafeString('<img class="artist-poster placeholder-image" src="{0}">'.format(placeholder));
});
Handlebars.registerHelper('MBUrl', function() {
return 'https://musicbrainz.org/artist/' + this.mbId;
});
@ -32,25 +34,25 @@ Handlebars.registerHelper('discogsUrl', function() {
});
Handlebars.registerHelper('allMusicUrl', function() {
return 'http://www.allmusic.com/artist/' + this.amId;
return 'http://www.allmusic.com/artist/' + this.allMusicId;
});
Handlebars.registerHelper('route', function() {
return StatusModel.get('urlBase') + '/artist/' + this.nameSlug;
});
Handlebars.registerHelper('percentOfEpisodes', function() {
var episodeCount = this.episodeCount;
var episodeFileCount = this.episodeFileCount;
// Handlebars.registerHelper('percentOfEpisodes', function() {
// var episodeCount = this.episodeCount;
// var episodeFileCount = this.episodeFileCount;
var percent = 100;
// var percent = 100;
if (episodeCount > 0) {
percent = episodeFileCount / episodeCount * 100;
}
// if (episodeCount > 0) {
// percent = episodeFileCount / episodeCount * 100;
// }
return percent;
});
// return percent;
// });
Handlebars.registerHelper('percentOfTracks', function() {
var trackCount = this.trackCount;

@ -6,6 +6,7 @@ require('./Helpers/Numbers');
require('./Helpers/Episode');
//require('./Helpers/Series');
require('./Helpers/Artist');
require('./Helpers/Album');
require('./Helpers/Quality');
require('./Helpers/System');
require('./Helpers/EachReverse');

Loading…
Cancel
Save