Compare commits
2 Commits
722183047d
...
85b3cb6852
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b3cb6852 | ||
|
|
bfc0675f5a |
31
.github/workflows/docker-publish.yml
vendored
31
.github/workflows/docker-publish.yml
vendored
@@ -2,6 +2,8 @@ name: Publish Server Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
@@ -29,22 +31,29 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,format=short
|
||||
- name: Determine version and tags
|
||||
id: info
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
||||
SHORT_SHA="$(echo '${{ github.sha }}' | cut -c1-7)"
|
||||
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
TAG="${{ github.ref_name }}"
|
||||
VERSION="${TAG#v}"
|
||||
echo "tags=${IMAGE}:${VERSION},${IMAGE}:latest" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "tags=${IMAGE}:trunk,${IMAGE}:${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.info.outputs.tags }}
|
||||
build-args: |
|
||||
FURUMI_VERSION=${{ steps.info.outputs.version }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -14,8 +14,10 @@ WORKDIR /usr/src/app
|
||||
# Option: Copy in root workspace files and source crates
|
||||
COPY . .
|
||||
|
||||
ARG FURUMI_VERSION=dev
|
||||
|
||||
# Build only the server for release
|
||||
RUN cargo build --release --bin furumi-server
|
||||
RUN FURUMI_VERSION=${FURUMI_VERSION} cargo build --release --bin furumi-server
|
||||
|
||||
# Stage 2: Create the minimal runtime image
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@@ -75,7 +75,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
MountOption::NoExec, // Better security for media mount
|
||||
];
|
||||
|
||||
println!("Mounting Furumi-ng v{} to {:?}", env!("CARGO_PKG_VERSION"), args.mount);
|
||||
println!("Mounting Furumi-ng v{} to {:?}", option_env!("FURUMI_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")), args.mount);
|
||||
|
||||
// Use Session + BackgroundSession for graceful unmount on exit
|
||||
let session = Session::new(fuse_fs, &args.mount, &options)?;
|
||||
|
||||
@@ -108,7 +108,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("Mounted Furumi-ng v{} to {:?}", env!("CARGO_PKG_VERSION"), mount_path);
|
||||
println!("Mounted Furumi-ng v{} to {:?}", option_env!("FURUMI_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")), mount_path);
|
||||
|
||||
// Wait for shutdown signal
|
||||
while running.load(Ordering::SeqCst) {
|
||||
|
||||
@@ -108,7 +108,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let svc = RemoteFileSystemServer::with_interceptor(remote_fs, auth.clone());
|
||||
|
||||
// Print startup info
|
||||
println!("Furumi-ng Server v{} listening on {}", env!("CARGO_PKG_VERSION"), addr);
|
||||
println!("Furumi-ng Server v{} listening on {}", option_env!("FURUMI_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")), addr);
|
||||
if args.no_tls {
|
||||
println!("WARNING: TLS is DISABLED — traffic is unencrypted");
|
||||
} else {
|
||||
|
||||
@@ -61,6 +61,6 @@ async fn player_html(
|
||||
) -> axum::response::Html<String> {
|
||||
let html = include_str!("player.html")
|
||||
.replace("<!-- USERNAME_PLACEHOLDER -->", &user_info.0)
|
||||
.replace("<!-- VERSION_PLACEHOLDER -->", env!("CARGO_PKG_VERSION"));
|
||||
.replace("<!-- VERSION_PLACEHOLDER -->", option_env!("FURUMI_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")));
|
||||
axum::response::Html(html)
|
||||
}
|
||||
|
||||
@@ -238,14 +238,15 @@ body {
|
||||
.queue-item .qi-info { flex: 1; overflow: hidden; }
|
||||
.queue-item .qi-title { font-size: 0.875rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.queue-item .qi-artist { font-size: 0.75rem; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.queue-item .qi-dur { font-size: 0.72rem; color: var(--text-muted); flex-shrink: 0; }
|
||||
.queue-item .qi-remove {
|
||||
opacity: 0; font-size: 0.8rem; color: var(--text-muted);
|
||||
background: none; border: none; cursor: pointer; padding: 2px 5px;
|
||||
border-radius: 4px; transition: all 0.15s;
|
||||
.queue-item .qi-dur { font-size: 0.75rem; color: var(--text-muted); margin-left: auto; margin-right: 0.5rem; }
|
||||
.qi-remove, .qi-locate {
|
||||
background: none; border: none; font-size: 0.9rem;
|
||||
color: var(--text-muted); cursor: pointer; padding: 0.3rem;
|
||||
border-radius: 4px; transition: all 0.2s; opacity: 0;
|
||||
}
|
||||
.queue-item:hover .qi-remove { opacity: 1; }
|
||||
.queue-item .qi-remove:hover { color: var(--danger); }
|
||||
.queue-item:hover .qi-remove, .queue-item:hover .qi-locate { opacity: 1; }
|
||||
.qi-remove:hover { background: rgba(248,113,113,0.15); color: var(--danger); }
|
||||
.qi-locate:hover { background: rgba(124,106,247,0.15); color: var(--primary); }
|
||||
.queue-item.dragging { opacity: 0.5; background: var(--bg-active); }
|
||||
.queue-item.drag-over { border-top: 2px solid var(--accent); margin-top: -2px; }
|
||||
|
||||
@@ -772,7 +773,7 @@ async function loadMeta(track) {
|
||||
if (res.ok) {
|
||||
const meta = await res.json();
|
||||
metaCache[track.path] = meta;
|
||||
track.meta = meta;
|
||||
if (queue[idx]) { queue[idx].meta = meta; }
|
||||
updateNowPlaying(track);
|
||||
renderQueue();
|
||||
}
|
||||
@@ -840,6 +841,7 @@ function renderQueue() {
|
||||
<div class="qi-artist">${esc(artist)}</div>
|
||||
</div>
|
||||
<span class="qi-dur">${dur}</span>
|
||||
<button class="qi-locate" title="Go to folder" onclick="locateTrack(${origIdx}, event)">📂</button>
|
||||
<button class="qi-remove" title="Remove track" onclick="removeFromQueue(${origIdx}, event)">✕</button>
|
||||
`;
|
||||
div.addEventListener('click', () => playIndex(origIdx));
|
||||
@@ -878,22 +880,45 @@ function scrollQueueToActive() {
|
||||
}
|
||||
|
||||
function removeFromQueue(origIdx, ev) {
|
||||
ev.stopPropagation();
|
||||
if (ev) ev.stopPropagation();
|
||||
const isPlaying = origIdx === queueIndex;
|
||||
|
||||
if (isPlaying) {
|
||||
queueIndex = -1;
|
||||
audio.pause();
|
||||
audio.src = '';
|
||||
updateNowPlaying();
|
||||
} else if (queueIndex > origIdx) {
|
||||
queueIndex--;
|
||||
}
|
||||
|
||||
queue.splice(origIdx, 1);
|
||||
if (queueIndex === origIdx) { queueIndex = -1; audio.pause(); audio.src = ''; }
|
||||
else if (queueIndex > origIdx) queueIndex--;
|
||||
|
||||
if (shuffle) {
|
||||
const sidx = shuffleOrder.indexOf(origIdx);
|
||||
if (sidx !== -1) shuffleOrder.splice(sidx, 1);
|
||||
for (let i = 0; i < shuffleOrder.length; i++) {
|
||||
if (shuffleOrder[i] > origIdx) shuffleOrder[i]--;
|
||||
if (shuffleOrder[i] > origIdx) shuffleOrder[i]--;
|
||||
}
|
||||
}
|
||||
|
||||
renderQueue();
|
||||
}
|
||||
|
||||
function locateTrack(idx, ev) {
|
||||
if (ev) ev.stopPropagation();
|
||||
const track = queue[idx];
|
||||
if (!track) return;
|
||||
const parts = track.path.split('/');
|
||||
parts.pop(); // remove filename
|
||||
const folder = parts.join('/');
|
||||
navigate(folder);
|
||||
if (window.innerWidth <= 768) {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar.classList.contains('open')) toggleSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
function moveQueueItem(fromPos, toPos) {
|
||||
if (fromPos === toPos) return;
|
||||
|
||||
@@ -1061,6 +1086,12 @@ const trackPath = urlParams.get('t');
|
||||
if (trackPath) {
|
||||
const parts = trackPath.split('/');
|
||||
const name = parts[parts.length - 1];
|
||||
|
||||
// Navigate to folder
|
||||
const folderParts = [...parts];
|
||||
folderParts.pop(); // remove name
|
||||
navigate(folderParts.join('/'));
|
||||
|
||||
addToQueue(trackPath, name, true);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user