Skip to content

Commit

Permalink
APIv3 support for stream scrobbling and External Subtitles
Browse files Browse the repository at this point in the history
  • Loading branch information
da3dsoul committed Nov 21, 2023
1 parent f63578f commit 6864ebb
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 3 deletions.
72 changes: 69 additions & 3 deletions Shoko.Server/API/v3/Controllers/FileController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
Expand All @@ -12,6 +14,7 @@
using Shoko.Server.API.ModelBinders;
using Shoko.Server.API.v3.Helpers;
using Shoko.Server.API.v3.Models.Common;
using Shoko.Server.API.v3.Models.Shoko;
using Shoko.Server.Models;
using Shoko.Server.Providers.TraktTV;
using Shoko.Server.Commands;
Expand All @@ -26,6 +29,7 @@
using MediaInfo = Shoko.Server.API.v3.Models.Shoko.MediaInfo;
using DataSource = Shoko.Server.API.v3.Models.Common.DataSource;
using Shoko.Server.Utilities;
using EpisodeType = Shoko.Models.Enums.EpisodeType;

namespace Shoko.Server.API.v3.Controllers;

Expand Down Expand Up @@ -472,15 +476,21 @@ public ActionResult RescanFileByAniDBFileID([FromRoute] int anidbFileID, [FromQu
/// Returns a file stream for the specified file ID.
/// </summary>
/// <param name="fileID">Shoko ID</param>
/// <param name="filename">Can use this to select a specific place (if the name is different). This is mostly used as a hint for players</param>
/// <param name="streamPositionScrobbling">If this is enabled, then the file is marked as watched when the stream reaches the end.
/// This is not a good way to scrobble, but it allows for players without plugin support to have an option to scrobble.
/// The readahead buffer on the player would determine the required percentage to scrobble.</param>
/// <returns>A file stream for the specified file.</returns>
[HttpGet("{fileID}/Stream")]
public ActionResult GetFileStream([FromRoute] int fileID)
[HttpGet("{fileID}/StreamDirectory/{filename}")]
public ActionResult GetFileStream([FromRoute] int fileID, [FromRoute] string filename = null, [FromQuery] bool streamPositionScrobbling = false)
{
var file = RepoFactory.VideoLocal.GetByID(fileID);
if (file == null)
return NotFound(FileNotFoundWithFileID);

var bestLocation = file.GetBestVideoLocalPlace();
var bestLocation = file.Places.FirstOrDefault(a => a.FileName.Equals(filename));

This comment has been minimized.

Copy link
@revam

revam Nov 21, 2023

Member

Why this change? This isn't the "best location" anymore.

This comment has been minimized.

Copy link
@da3dsoul

da3dsoul Nov 21, 2023

Author Member

It'll only choose a different one if you specifically choose a different one, otherwise it falls back on the best place

bestLocation ??= file.GetBestVideoLocalPlace();

var fileInfo = bestLocation.GetFile();
if (fileInfo == null)
Expand All @@ -490,7 +500,63 @@ public ActionResult GetFileStream([FromRoute] int fileID)
if (!provider.TryGetContentType(fileInfo.FullName, out var contentType))
contentType = "application/octet-stream";

return PhysicalFile(fileInfo.FullName, contentType, enableRangeProcessing: true);
if (streamPositionScrobbling)
{
var scrobbleFile = new ScrobblingFileResult(file, User.JMMUserID, fileInfo.FullName, contentType)
{
FileDownloadName = filename ?? fileInfo.Name
};
return scrobbleFile;
}

var physicalFile = PhysicalFile(fileInfo.FullName, contentType);

This comment has been minimized.

Copy link
@revam

revam Nov 21, 2023

Member

Please re-enable range processing. It is actually a useful feature as it allows us to steeam the file in chuncks.

This comment has been minimized.

Copy link
@revam

revam Nov 21, 2023

Member

We can scrobble the file using the dedicated scrobble endpoint so it's fine even if we won't get the hackery scrobble event when using it.

This comment has been minimized.

Copy link
@da3dsoul

da3dsoul Nov 21, 2023

Author Member

That was a mistake. Didn't mean to remove it

This comment has been minimized.

Copy link
@da3dsoul

da3dsoul Nov 21, 2023

Author Member

We can scrobble the file using the dedicated scrobble endpoint so it's fine even if we won't get the hackery scrobble event when using it.

You didn't read the comment, clearly

This comment has been minimized.

Copy link
@revam

revam Nov 21, 2023

Member

I read it. Which is why i said "when using it" where "it" is partial range support. Since it seems incompatible with the hackery done to determine if the full file is played.

This comment has been minimized.

Copy link
@revam

revam Nov 21, 2023

Member

If the player doesn't support custom scrobbling but needs chunked streams to work then we might need a different solution, but that's for later if we get there.

This comment has been minimized.

Copy link
@da3dsoul

da3dsoul Nov 21, 2023

Author Member

Oh... Yes... It needs to detect if it was requesting the end of the file before doing that. Good catch

This comment has been minimized.

Copy link
@da3dsoul

da3dsoul Nov 21, 2023

Author Member

I didn't understand what you meant

This comment has been minimized.

Copy link
@revam

revam Nov 21, 2023

Member

Supporting legacy stuff is a PITA. 😔

physicalFile.FileDownloadName = filename ?? fileInfo.Name;
return physicalFile;
}

/// <summary>
/// Returns the external subtitles for a file
/// </summary>
/// <param name="fileID">Shoko ID</param>
/// <returns>A file stream for the specified file.</returns>
[HttpGet("{fileID}/StreamDirectory/")]
public ActionResult GetFileStreamDirectory([FromRoute] int fileID)
{
var file = RepoFactory.VideoLocal.GetByID(fileID);
if (file == null)
return NotFound(FileNotFoundWithFileID);

var routeTemplate = Request.Scheme + "://" + Request.Host + "/api/v3/File/" + fileID + "/StreamDirectory/ExternalSub/";
return new ObjectResult("<table>" + string.Join(string.Empty,
file.Media.TextStreams.Where(a => a.External).Select(a => $"<tr><td><a href=\"{routeTemplate + a.Filename}\"/></td></tr>")) + "</table>");
}

/// <summary>
/// Gets an external subtitle file
/// </summary>
/// <param name="fileID"></param>
/// <param name="filename"></param>
/// <returns></returns>
[HttpGet("{fileID}/StreamDirectory/ExternalSub/{filename}")]
public ActionResult GetExternalSubtitle([FromRoute] int fileID, [FromRoute] string filename)
{
var file = RepoFactory.VideoLocal.GetByID(fileID);
if (file == null)
return NotFound(FileNotFoundWithFileID);


foreach (var place in file.Places)
{
var path = place.GetFile()?.Directory?.FullName;
if (path == null) continue;
path = Path.Combine(path, filename);
var subFile = new FileInfo(path);
if (!subFile.Exists) continue;

return PhysicalFile(subFile.FullName, "application/octet-stream");
}

return NotFound();
}

/// <summary>
Expand Down
35 changes: 35 additions & 0 deletions Shoko.Server/API/v3/Models/Shoko/ScrobblingFileResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Shoko.Server.Models;

namespace Shoko.Server.API.v3.Models.Shoko;

public class ScrobblingFileResult : PhysicalFileResult
{
private SVR_VideoLocal VideoLocal { get; set; }
private int UserID { get; set; }
public ScrobblingFileResult(SVR_VideoLocal videoLocal, int userID, string fileName, string contentType) : base(fileName, contentType)
{
VideoLocal = videoLocal;
UserID = userID;
EnableRangeProcessing = true;
}

public ScrobblingFileResult(SVR_VideoLocal videoLocal, int userID, string fileName, MediaTypeHeaderValue contentType) : base(fileName, contentType)
{
VideoLocal = videoLocal;
UserID = userID;
EnableRangeProcessing = true;
}

public override async Task ExecuteResultAsync(ActionContext context)
{
await base.ExecuteResultAsync(context);
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Factory.StartNew(() => VideoLocal.ToggleWatchedStatus(true, UserID), new CancellationToken(), TaskCreationOptions.LongRunning,
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
TaskScheduler.Default);
}
}

0 comments on commit 6864ebb

Please sign in to comment.