Compare commits
2 Commits
2f5a182afd
...
046bf6afe4
| Author | SHA1 | Date | |
|---|---|---|---|
| 046bf6afe4 | |||
| 83741d7980 |
63
.gitea/workflows/build.yaml
Normal file
63
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,63 @@
|
||||
name: 'Build Plugin'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET 9
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Verify .NET installation
|
||||
run: dotnet --version
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore Jellyfin.Plugin.JellyLMS.sln
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet build Jellyfin.Plugin.JellyLMS.sln --configuration Release --no-restore --no-self-contained
|
||||
|
||||
- name: Install JPRM
|
||||
run: |
|
||||
python3 -m venv /tmp/jprm-venv
|
||||
/tmp/jprm-venv/bin/pip install jprm
|
||||
|
||||
- name: Build Jellyfin Plugin
|
||||
id: jprm
|
||||
run: |
|
||||
# Create artifacts directory for JPRM output
|
||||
mkdir -p artifacts
|
||||
|
||||
# Build plugin using JPRM
|
||||
/tmp/jprm-venv/bin/jprm --verbosity=debug plugin build .
|
||||
|
||||
# Find the generated zip file
|
||||
ARTIFACT=$(find . -name "*.zip" -type f -print -quit)
|
||||
echo "artifact=${ARTIFACT}" >> $GITHUB_OUTPUT
|
||||
echo "Found artifact: ${ARTIFACT}"
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jellylms-plugin
|
||||
path: ${{ steps.jprm.outputs.artifact }}
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
130
.gitea/workflows/release.yaml
Normal file
130
.gitea/workflows/release.yaml
Normal file
@ -0,0 +1,130 @@
|
||||
name: 'Release Plugin'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (e.g., v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET 9
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Verify .NET installation
|
||||
run: dotnet --version
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
else
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
fi
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
|
||||
echo "Building version: ${VERSION}"
|
||||
|
||||
- name: Update build.yaml with version
|
||||
run: |
|
||||
VERSION="${{ steps.get_version.outputs.version_number }}"
|
||||
sed -i "s/^version:.*/version: \"${VERSION}\"/" build.yaml
|
||||
cat build.yaml
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore Jellyfin.Plugin.JellyLMS.sln
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet build Jellyfin.Plugin.JellyLMS.sln --configuration Release --no-restore --no-self-contained
|
||||
|
||||
- name: Install JPRM
|
||||
run: |
|
||||
python3 -m venv /tmp/jprm-venv
|
||||
/tmp/jprm-venv/bin/pip install jprm
|
||||
|
||||
- name: Build Jellyfin Plugin
|
||||
id: jprm
|
||||
run: |
|
||||
# Create artifacts directory for JPRM output
|
||||
mkdir -p artifacts
|
||||
|
||||
# Build plugin using JPRM
|
||||
/tmp/jprm-venv/bin/jprm --verbosity=debug plugin build ./
|
||||
|
||||
# Find the generated zip file
|
||||
ARTIFACT=$(find . -name "*.zip" -type f -print -quit)
|
||||
ARTIFACT_NAME=$(basename "${ARTIFACT}")
|
||||
echo "artifact=${ARTIFACT}" >> $GITHUB_OUTPUT
|
||||
echo "artifact_name=${ARTIFACT_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "Found artifact: ${ARTIFACT}"
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Get repository information
|
||||
REPO_OWNER="${{ github.repository_owner }}"
|
||||
REPO_NAME="${{ github.event.repository.name }}"
|
||||
GITEA_URL="${{ github.server_url }}"
|
||||
|
||||
# Prepare release body
|
||||
RELEASE_BODY="JellyLMS Jellyfin Plugin ${{ steps.get_version.outputs.version }}\n\nSee attached files for plugin installation."
|
||||
RELEASE_BODY_JSON=$(echo -n "${RELEASE_BODY}" | jq -Rs .)
|
||||
|
||||
# Create release using Gitea API
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases" \
|
||||
-d "{
|
||||
\"tag_name\": \"${{ steps.get_version.outputs.version }}\",
|
||||
\"name\": \"Release ${{ steps.get_version.outputs.version }}\",
|
||||
\"body\": ${RELEASE_BODY_JSON},
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
RELEASE_ID=$(echo "$BODY" | jq -r '.id')
|
||||
echo "Created release with ID: ${RELEASE_ID}"
|
||||
else
|
||||
echo "Failed to create release. HTTP ${HTTP_CODE}"
|
||||
echo "$BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Upload plugin artifact
|
||||
echo "Uploading plugin artifact..."
|
||||
curl -f -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/zip" \
|
||||
--data-binary "@${{ steps.jprm.outputs.artifact }}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=${{ steps.jprm.outputs.artifact_name }}"
|
||||
|
||||
# Upload build.yaml
|
||||
echo "Uploading build.yaml..."
|
||||
curl -f -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/x-yaml" \
|
||||
--data-binary "@build.yaml" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=build.yaml"
|
||||
|
||||
echo "Release created successfully!"
|
||||
echo "View at: ${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/tag/${{ steps.get_version.outputs.version }}"
|
||||
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.JellyLMS.Models;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -219,6 +221,72 @@ public class LmsDeviceDiscoveryService : IHostedService, IDisposable
|
||||
player.MacAddress,
|
||||
session.Id);
|
||||
}
|
||||
|
||||
// Report playback progress if the controller has an active item
|
||||
if (controller is LmsSessionController lmsController)
|
||||
{
|
||||
await ReportPlaybackProgressAsync(sessionManager, session, lmsController).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReportPlaybackProgressAsync(
|
||||
ISessionManager sessionManager,
|
||||
SessionInfo session,
|
||||
LmsSessionController controller)
|
||||
{
|
||||
if (!controller.IsPlaying || !controller.CurrentItemId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get current playback status from LMS
|
||||
var status = await _lmsClient.GetPlayerStatusAsync(controller.PlayerMac).ConfigureAwait(false);
|
||||
if (status == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the item from Jellyfin library
|
||||
var libraryManager = _serviceProvider.GetService<ILibraryManager>();
|
||||
if (libraryManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = libraryManager.GetItemById(controller.CurrentItemId.Value);
|
||||
if (item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate position in ticks
|
||||
var positionTicks = (long)(status.Time * TimeSpan.TicksPerSecond);
|
||||
|
||||
// Determine if paused
|
||||
var isPaused = status.Mode == "pause";
|
||||
|
||||
// Create playback progress info
|
||||
var progressInfo = new PlaybackProgressInfo
|
||||
{
|
||||
ItemId = controller.CurrentItemId.Value,
|
||||
SessionId = session.Id,
|
||||
IsPaused = isPaused,
|
||||
PositionTicks = positionTicks,
|
||||
PlayMethod = PlayMethod.DirectStream,
|
||||
CanSeek = true,
|
||||
IsMuted = status.Volume == 0,
|
||||
VolumeLevel = status.Volume
|
||||
};
|
||||
|
||||
// Report progress to session manager
|
||||
await sessionManager.OnPlaybackProgress(progressInfo).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Error reporting playback progress for player {Name}", controller.PlayerMac);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -39,8 +39,23 @@ public class LmsSessionController : ISessionController
|
||||
_session = session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the currently playing item ID.
|
||||
/// </summary>
|
||||
public Guid? CurrentItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether playback is currently active.
|
||||
/// </summary>
|
||||
public bool IsPlaying { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether playback is paused.
|
||||
/// </summary>
|
||||
public bool IsPaused { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsSessionActive => _player.IsConnected && _player.IsPoweredOn;
|
||||
public bool IsSessionActive => _player.IsConnected;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsMediaControl => true;
|
||||
@ -57,10 +72,11 @@ public class LmsSessionController : ISessionController
|
||||
T data,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"LMS Session Controller received message {MessageType} for player {PlayerName}",
|
||||
_logger.LogInformation(
|
||||
"LMS Session Controller received message {MessageType} for player {PlayerName} ({Mac})",
|
||||
name,
|
||||
_player.Name);
|
||||
_player.Name,
|
||||
_player.MacAddress);
|
||||
|
||||
try
|
||||
{
|
||||
@ -118,6 +134,11 @@ public class LmsSessionController : ISessionController
|
||||
_logger.LogInformation("Playing stream URL: {Url}", streamUrl);
|
||||
await _lmsClient.PlayUrlAsync(_player.MacAddress, streamUrl).ConfigureAwait(false);
|
||||
|
||||
// Track current playback state
|
||||
CurrentItemId = itemId;
|
||||
IsPlaying = true;
|
||||
IsPaused = false;
|
||||
|
||||
// Seek to start position if specified
|
||||
if (playRequest.StartPositionTicks.HasValue && playRequest.StartPositionTicks.Value > 0)
|
||||
{
|
||||
@ -135,29 +156,60 @@ public class LmsSessionController : ISessionController
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Playstate command {Command} for player {PlayerName}",
|
||||
_logger.LogInformation(
|
||||
"Playstate command {Command} for player {PlayerName} ({Mac})",
|
||||
playstateRequest.Command,
|
||||
_player.Name);
|
||||
_player.Name,
|
||||
_player.MacAddress);
|
||||
|
||||
switch (playstateRequest.Command)
|
||||
{
|
||||
case PlaystateCommand.Stop:
|
||||
await _lmsClient.StopAsync(_player.MacAddress).ConfigureAwait(false);
|
||||
var stopResult = await _lmsClient.StopAsync(_player.MacAddress).ConfigureAwait(false);
|
||||
_logger.LogInformation("Stop command result: {Result}", stopResult);
|
||||
IsPlaying = false;
|
||||
IsPaused = false;
|
||||
CurrentItemId = null;
|
||||
break;
|
||||
|
||||
case PlaystateCommand.Pause:
|
||||
await _lmsClient.PauseAsync(_player.MacAddress).ConfigureAwait(false);
|
||||
var pauseResult = await _lmsClient.PauseAsync(_player.MacAddress).ConfigureAwait(false);
|
||||
_logger.LogInformation("Pause command result: {Result}", pauseResult);
|
||||
IsPaused = true;
|
||||
break;
|
||||
|
||||
case PlaystateCommand.Unpause:
|
||||
await _lmsClient.PlayAsync(_player.MacAddress).ConfigureAwait(false);
|
||||
var playResult = await _lmsClient.PlayAsync(_player.MacAddress).ConfigureAwait(false);
|
||||
_logger.LogInformation("Unpause/Play command result: {Result}", playResult);
|
||||
IsPaused = false;
|
||||
break;
|
||||
|
||||
case PlaystateCommand.PlayPause:
|
||||
// Toggle play/pause - check current state first
|
||||
var currentState = await _lmsClient.GetPlayerStatusAsync(_player.MacAddress).ConfigureAwait(false);
|
||||
if (currentState?.Mode == "play")
|
||||
{
|
||||
var togglePauseResult = await _lmsClient.PauseAsync(_player.MacAddress).ConfigureAwait(false);
|
||||
_logger.LogInformation("PlayPause toggle (pause) result: {Result}", togglePauseResult);
|
||||
IsPaused = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var togglePlayResult = await _lmsClient.PlayAsync(_player.MacAddress).ConfigureAwait(false);
|
||||
_logger.LogInformation("PlayPause toggle (play) result: {Result}", togglePlayResult);
|
||||
IsPaused = false;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PlaystateCommand.Seek:
|
||||
if (playstateRequest.SeekPositionTicks.HasValue)
|
||||
{
|
||||
var positionSeconds = playstateRequest.SeekPositionTicks.Value / TimeSpan.TicksPerSecond;
|
||||
_logger.LogInformation(
|
||||
"Seeking player {PlayerName} to {Seconds} seconds",
|
||||
_player.Name,
|
||||
positionSeconds);
|
||||
await _lmsClient.SeekAsync(_player.MacAddress, positionSeconds).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
name: "JellyLMS"
|
||||
guid: "a5b8c9d0-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
|
||||
version: "1.0.0.0"
|
||||
targetAbi: "10.9.0.0"
|
||||
framework: "net8.0"
|
||||
targetAbi: "10.11.0.0"
|
||||
framework: "net9.0"
|
||||
overview: "Bridge plugin to stream Jellyfin audio to Logitech Media Server (LMS) players"
|
||||
description: >
|
||||
JellyLMS enables Jellyfin to stream audio to LMS (Logitech Media Server) for
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user