Fetch media info and provide metadata #153

Merged
Kevinjil merged 6 commits from feature/media-info into master 2025-01-31 21:21:17 +00:00
16 changed files with 410 additions and 34 deletions

View File

@@ -35,7 +35,7 @@ namespace Jellyfin.Xtream;
/// The Xtream Codes API channel.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
public class CatchupChannel(ILogger<CatchupChannel> logger) : IChannel
public class CatchupChannel(ILogger<CatchupChannel> logger) : IChannel, IDisableMediaSourceDisplay
{
private readonly ILogger<CatchupChannel> _logger = logger;

View File

@@ -0,0 +1,46 @@
// Copyright (C) 2022 Kevin Jilissen
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Newtonsoft.Json;
#pragma warning disable CS1591
namespace Jellyfin.Xtream.Client.Models;
public class AudioInfo
{
[JsonProperty("index")]
public int Index { get; set; }
[JsonProperty("codec_name")]
public string CodecName { get; set; } = string.Empty;
[JsonProperty("profile")]
public string Profile { get; set; } = string.Empty;
[JsonProperty("sample_fmt")]
public string SampleFormat { get; set; } = string.Empty;
[JsonProperty("sample_rate")]
public int SampleRate { get; set; }
[JsonProperty("channels")]
public int Channels { get; set; }
[JsonProperty("channel_layout")]
public string ChannelLayout { get; set; } = string.Empty;
[JsonProperty("bit_rate")]
public int Bitrate { get; set; }
}

View File

@@ -22,20 +22,28 @@ namespace Jellyfin.Xtream.Client.Models;
public class EpisodeInfo
{
[JsonProperty("movie_image")]
public string MovieImage { get; set; } = string.Empty;
public string? MovieImage { get; set; }
[JsonProperty("plot")]
public string Plot { get; set; } = string.Empty;
public string? Plot { get; set; }
[JsonProperty("releasedate")]
public DateTime ReleaseDate { get; set; }
public DateTime? ReleaseDate { get; set; }
[JsonProperty("rating")]
public decimal Rating { get; set; }
public decimal? Rating { get; set; }
[JsonProperty("duration_secs")]
public int DurationSecs { get; set; }
public int? DurationSecs { get; set; }
[JsonProperty("bitrate")]
public int Bitrate { get; set; }
public int? Bitrate { get; set; }
[JsonProperty("video")]
[JsonConverter(typeof(OnlyObjectConverter<VideoInfo>))]
public VideoInfo? Video { get; set; }
[JsonProperty("audio")]
[JsonConverter(typeof(OnlyObjectConverter<AudioInfo>))]
public AudioInfo? Audio { get; set; }
}

View File

@@ -0,0 +1,64 @@
// Copyright (C) 2022 Kevin Jilissen
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Newtonsoft.Json;
#pragma warning disable CS1591
namespace Jellyfin.Xtream.Client.Models;
public class VideoInfo
{
[JsonProperty("index")]
public int Index { get; set; }
[JsonProperty("codec_name")]
public string CodecName { get; set; } = string.Empty;
[JsonProperty("profile")]
public string Profile { get; set; } = string.Empty;
[JsonProperty("width")]
public int Width { get; set; }
[JsonProperty("height")]
public int Height { get; set; }
[JsonProperty("display_aspect_ratio")]
public string AspectRatio { get; set; } = string.Empty;
[JsonProperty("pix_fmt")]
public string PixelFormat { get; set; } = string.Empty;
[JsonProperty("level")]
public int Level { get; set; }
[JsonProperty("color_range")]
public string ColorRange { get; set; } = string.Empty;
[JsonProperty("color_space")]
public string ColorSpace { get; set; } = string.Empty;
[JsonProperty("color_transfer")]
public string ColorTransfer { get; set; } = string.Empty;
[JsonProperty("color_primaries")]
public string ColorPrimaries { get; set; } = string.Empty;
[JsonProperty("is_avc")]
public bool IsAVC { get; set; }
[JsonProperty("bits_per_raw_sample")]
public int BitsPerRawSample { get; set; }
}

View File

@@ -0,0 +1,58 @@
// Copyright (C) 2022 Kevin Jilissen
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using System;
using Newtonsoft.Json;
#pragma warning disable CS1591
namespace Jellyfin.Xtream.Client.Models;
public class VodInfo
{
[JsonProperty("movie_image")]
public string? MovieImage { get; set; }
[JsonProperty("genre")]
public string? Genre { get; set; }
[JsonProperty("plot")]
public string? Plot { get; set; }
[JsonProperty("director")]
public string? Director { get; set; }
[JsonProperty("rating")]
public decimal? Rating { get; set; }
[JsonProperty("releasedate")]
public DateTime? ReleaseDate { get; set; }
[JsonProperty("duration_secs")]
public int? DurationSecs { get; set; }
[JsonProperty("tmdb_id")]
public int? TmdbId { get; set; }
[JsonProperty("bitrate")]
public int Bitrate { get; set; }
[JsonProperty("video")]
[JsonConverter(typeof(OnlyObjectConverter<VideoInfo>))]
public VideoInfo? Video { get; set; }
[JsonProperty("audio")]
[JsonConverter(typeof(OnlyObjectConverter<AudioInfo>))]
public AudioInfo? Audio { get; set; }
}

View File

@@ -0,0 +1,30 @@
// Copyright (C) 2022 Kevin Jilissen
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Newtonsoft.Json;
#pragma warning disable CS1591
namespace Jellyfin.Xtream.Client.Models;
public class VodStreamInfo
{
[JsonProperty("info")]
[JsonConverter(typeof(OnlyObjectConverter<VodInfo>))]
public VodInfo? Info { get; set; }
[JsonProperty("movie_data")]
[JsonConverter(typeof(OnlyObjectConverter<StreamInfo>))]
public StreamInfo? MovieData { get; set; }
}

View File

@@ -71,6 +71,12 @@ public class XtreamClient(HttpClient client) : IDisposable
$"/player_api.php?username={connectionInfo.UserName}&password={connectionInfo.Password}&action=get_vod_streams&category_id={categoryId}",
cancellationToken);
public Task<VodStreamInfo> GetVodInfoAsync(ConnectionInfo connectionInfo, int streamId, CancellationToken cancellationToken) =>
QueryApi<VodStreamInfo>(
connectionInfo,
$"/player_api.php?username={connectionInfo.UserName}&password={connectionInfo.Password}&action=get_vod_info&vod_id={streamId}",
cancellationToken);
public Task<List<StreamInfo>> GetLiveStreamsByCategoryAsync(ConnectionInfo connectionInfo, int categoryId, CancellationToken cancellationToken) =>
QueryApi<List<StreamInfo>>(
connectionInfo,

View File

@@ -54,6 +54,11 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary>
public bool IsVodVisible { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the Video On-demand channel is visible.
/// </summary>
public bool IsTmdbVodOverride { get; set; } = true;
/// <summary>
/// Gets or sets the channels displayed in Live TV.
/// </summary>

View File

@@ -9,6 +9,12 @@
<span>Show this channel to users</span>
</label>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input is="emby-checkbox" id="TmdbOverride" name="TmdbOverride" type="checkbox" />
<span>Override metadata with TMDB</span>
</label>
</div>
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">Video On-demand selection</h2>
</div>

View File

@@ -11,6 +11,8 @@ export default function (view) {
const getConfig = ApiClient.getPluginConfiguration(pluginId);
const visible = view.querySelector("#Visible");
getConfig.then((config) => visible.checked = config.IsVodVisible);
const tmdbOverride = view.querySelector("#TmdbOverride");
getConfig.then((config) => TmdbOverride.checked = config.IsTmdbVodOverride);
const table = view.querySelector('#VodContent');
Xtream.populateCategoriesTable(
table,
@@ -23,6 +25,7 @@ export default function (view) {
ApiClient.getPluginConfiguration(pluginId).then((config) => {
config.IsVodVisible = visible.checked;
config.IsTmdbVodOverride = tmdbOverride.checked;
config.Vod = data;
ApiClient.updatePluginConfiguration(pluginId, config).then((result) => {
Dashboard.processPluginConfigurationUpdateResult(result);

View File

@@ -13,10 +13,12 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using Jellyfin.Xtream.Providers;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.Xtream;
@@ -31,5 +33,6 @@ public class PluginServiceRegistrator : IPluginServiceRegistrator
serviceCollection.AddSingleton<IChannel, CatchupChannel>();
serviceCollection.AddSingleton<IChannel, SeriesChannel>();
serviceCollection.AddSingleton<IChannel, VodChannel>();
serviceCollection.AddSingleton<IPreRefreshProvider, XtreamVodProvider>();
}
}

View File

@@ -0,0 +1,116 @@
// Copyright (C) 2022 Kevin Jilissen
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Xtream.Client;
using Jellyfin.Xtream.Client.Models;
using Jellyfin.Xtream.Service;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Xtream.Providers;
/// <summary>
/// The Xtream Codes VOD metadata provider.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
public class XtreamVodProvider(ILogger<VodChannel> logger, IProviderManager providerManager) : ICustomMetadataProvider<Movie>, IPreRefreshProvider
{
/// <summary>
/// The name of the provider.
/// </summary>
public const string ProviderName = "XtreamVodProvider";
/// <inheritdoc/>
public string Name => ProviderName;
/// <inheritdoc/>
public async Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
string? idStr = item.GetProviderId(ProviderName);
if (idStr is not null)
{
logger.LogDebug("Getting metadata for movie {Id}", idStr);
int id = int.Parse(idStr, CultureInfo.InvariantCulture);
using XtreamClient client = new();
VodStreamInfo vod = await client.GetVodInfoAsync(Plugin.Instance.Creds, id, cancellationToken).ConfigureAwait(false);
VodInfo? i = vod.Info;
if (i is null)
{
return ItemUpdateType.None;
}
item.Overview ??= i.Plot;
item.PremiereDate ??= i.ReleaseDate;
item.RunTimeTicks ??= i.DurationSecs is not null ? TimeSpan.TicksPerSecond * i.DurationSecs : null;
item.TotalBitrate ??= i.Bitrate;
if (i.Genre is string genres)
{
item.Genres ??= genres.Split(',').Select(genre => genre.Trim()).ToArray();
}
if (!item.HasProviderId(MetadataProvider.Tmdb))
{
if (i.TmdbId is int tmdbId)
{
options.ReplaceAllMetadata = true;
item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString(CultureInfo.InvariantCulture));
}
else if (Plugin.Instance.Configuration.IsTmdbVodOverride)
{
MovieInfo queryInfo = new()
{
Name = StreamService.ParseName(vod.MovieData?.Name ?? string.Empty).Title,
Year = item.PremiereDate?.Year,
};
// Try to fetch the TMDB id to get proper metadata.
RemoteSearchQuery<MovieInfo> query = new()
{
SearchInfo = queryInfo,
SearchProviderName = "TheMovieDb",
};
IEnumerable<RemoteSearchResult> results = await providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, cancellationToken).ConfigureAwait(false);
if (results.Any())
{
RemoteSearchResult tmdbMovie = results.First();
if (tmdbMovie.HasProviderId(MetadataProvider.Tmdb))
{
string? queryId = tmdbMovie.GetProviderId(MetadataProvider.Tmdb);
if (queryId is not null)
{
options.ReplaceAllMetadata = true;
item.SetProviderId(MetadataProvider.Tmdb, queryId);
}
}
}
}
}
}
return ItemUpdateType.MetadataImport;
}
}

View File

@@ -35,7 +35,7 @@ namespace Jellyfin.Xtream;
/// The Xtream Codes API channel.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
public class SeriesChannel(ILogger<SeriesChannel> logger) : IChannel
public class SeriesChannel(ILogger<SeriesChannel> logger) : IChannel, IDisableMediaSourceDisplay
{
/// <inheritdoc />
public string? Name => "Xtream Series";
@@ -198,20 +198,19 @@ public class SeriesChannel(ILogger<SeriesChannel> logger) : IChannel
{
Client.Models.SeriesInfo serie = series.Info;
ParsedName parsedName = StreamService.ParseName(episode.Title);
List<MediaSourceInfo> sources = [
Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Series, episode.EpisodeId, episode.ContainerExtension)
List<MediaSourceInfo> sources =
[
Plugin.Instance.StreamService.GetMediaSourceInfo(
StreamType.Series,
episode.EpisodeId,
episode.ContainerExtension,
videoInfo: episode.Info?.Video,
audioInfo: episode.Info?.Audio)
];
string? cover = episode.Info?.MovieImage;
if (string.IsNullOrEmpty(cover) && season != null)
{
cover = season.Cover;
}
if (string.IsNullOrEmpty(cover))
{
cover = serie.Cover;
}
cover ??= season?.Cover;
cover ??= serie.Cover;
return new()
{

View File

@@ -24,6 +24,7 @@ using Jellyfin.Xtream.Client;
using Jellyfin.Xtream.Client.Models;
using Jellyfin.Xtream.Configuration;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
@@ -367,6 +368,8 @@ public partial class StreamService
/// <param name="restream">Boolean indicating whether or not restreaming is used.</param>
/// <param name="start">The datetime representing the start time of catcup TV.</param>
/// <param name="durationMinutes">The duration in minutes of the catcup TV stream.</param>
/// <param name="videoInfo">The Xtream video info if known.</param>
/// <param name="audioInfo">The Xtream audio info if known.</param>
/// <returns>The media source info as <see cref="MediaSourceInfo"/> class.</returns>
public MediaSourceInfo GetMediaSourceInfo(
StreamType type,
@@ -374,7 +377,9 @@ public partial class StreamService
string? extension = null,
bool restream = false,
DateTime? start = null,
int durationMinutes = 0)
int durationMinutes = 0,
VideoInfo? videoInfo = null,
AudioInfo? audioInfo = null)
{
string prefix = string.Empty;
switch (type)
@@ -403,22 +408,41 @@ public partial class StreamService
bool isLive = type == StreamType.Live;
return new MediaSourceInfo()
{
Container = extension,
EncoderProtocol = MediaProtocol.Http,
Id = ToGuid(MediaSourcePrefix, (int)type, id, 0).ToString(),
IsInfiniteStream = isLive,
IsRemote = true,
// Define media sources with unknown index and interlaced to improve compatibility.
MediaStreams = [
MediaStreams =
[
new()
{
AspectRatio = videoInfo?.AspectRatio,
BitDepth = videoInfo?.BitsPerRawSample,
Codec = videoInfo?.CodecName,
ColorPrimaries = videoInfo?.ColorPrimaries,
ColorRange = videoInfo?.ColorRange,
ColorSpace = videoInfo?.ColorSpace,
ColorTransfer = videoInfo?.ColorTransfer,
Height = videoInfo?.Height,
Index = videoInfo?.Index ?? -1,
IsAVC = videoInfo?.IsAVC,
IsInterlaced = true,
Level = videoInfo?.Level,
PixelFormat = videoInfo?.PixelFormat,
Profile = videoInfo?.Profile,
Type = MediaStreamType.Video,
Index = -1,
IsInterlaced = true
},
new()
{
BitRate = audioInfo?.Bitrate,
ChannelLayout = audioInfo?.ChannelLayout,
Channels = audioInfo?.Channels,
Codec = audioInfo?.CodecName,
Index = audioInfo?.Index ?? -1,
Profile = audioInfo?.Profile,
SampleRate = audioInfo?.SampleRate,
Type = MediaStreamType.Audio,
Index = -1
}
],
Name = "default",

View File

@@ -20,6 +20,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Xtream.Client.Models;
using Jellyfin.Xtream.Providers;
using Jellyfin.Xtream.Service;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Providers;
@@ -34,7 +35,7 @@ namespace Jellyfin.Xtream;
/// The Xtream Codes API channel.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
public class VodChannel(ILogger<VodChannel> logger) : IChannel
public class VodChannel(ILogger<VodChannel> logger) : IChannel, IDisableMediaSourceDisplay
{
/// <inheritdoc />
public string? Name => "Xtream Video On-Demand";
@@ -113,19 +114,23 @@ public class VodChannel(ILogger<VodChannel> logger) : IChannel
}
}
private ChannelItemInfo CreateChannelItemInfo(StreamInfo stream)
private Task<ChannelItemInfo> CreateChannelItemInfo(StreamInfo stream)
{
long added = long.Parse(stream.Added, CultureInfo.InvariantCulture);
ParsedName parsedName = StreamService.ParseName(stream.Name);
List<MediaSourceInfo> sources = [
Plugin.Instance.StreamService.GetMediaSourceInfo(StreamType.Vod, stream.StreamId, stream.ContainerExtension)
List<MediaSourceInfo> sources =
[
Plugin.Instance.StreamService.GetMediaSourceInfo(
StreamType.Vod,
stream.StreamId,
stream.ContainerExtension)
];
return new()
ChannelItemInfo result = new ChannelItemInfo()
{
ContentType = ChannelMediaContentType.Movie,
DateCreated = DateTimeOffset.FromUnixTimeSeconds(added).DateTime,
FolderType = ChannelFolderType.Container,
Id = $"{StreamService.StreamPrefix}{stream.StreamId}",
ImageUrl = stream.StreamIcon,
IsLiveStream = false,
@@ -134,7 +139,10 @@ public class VodChannel(ILogger<VodChannel> logger) : IChannel
Name = parsedName.Title,
Tags = new List<string>(parsedName.Tags),
Type = ChannelItemType.Media,
ProviderIds = { { XtreamVodProvider.ProviderName, stream.StreamId.ToString(CultureInfo.InvariantCulture) } },
};
return Task.FromResult(result);
}
private async Task<ChannelItemResult> GetCategories(CancellationToken cancellationToken)
@@ -152,8 +160,8 @@ public class VodChannel(ILogger<VodChannel> logger) : IChannel
private async Task<ChannelItemResult> GetStreams(int categoryId, CancellationToken cancellationToken)
{
IEnumerable<StreamInfo> streams = await Plugin.Instance.StreamService.GetVodStreams(categoryId, cancellationToken).ConfigureAwait(false);
List<ChannelItemInfo> items = new List<ChannelItemInfo>(streams.Select(CreateChannelItemInfo));
ChannelItemResult result = new()
List<ChannelItemInfo> items = [.. await Task.WhenAll(streams.Select(CreateChannelItemInfo)).ConfigureAwait(false)];
ChannelItemResult result = new ChannelItemResult()
{
Items = items,
TotalRecordCount = items.Count

View File

@@ -63,7 +63,7 @@
<!-- disable warning CA1040: Avoid empty interfaces -->
<Rule Id="CA1040" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
<Rule Id="CA1062" Action="None" />
<!-- TODO: enable when false positives are fixed -->
<!-- disable warning CA1508: Avoid dead conditional code -->
<Rule Id="CA1508" Action="Info" />