mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Compare commits
4 Commits
42890bc526
...
451f0795ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
451f0795ba | ||
|
|
74dc7678f1 | ||
|
|
bbe8fe164d | ||
|
|
72969711d2 |
20
.github/workflows/pipeline.yml
vendored
20
.github/workflows/pipeline.yml
vendored
@ -159,6 +159,23 @@ jobs:
|
||||
done
|
||||
- run: ./.github/workflows/validate-translations.sh -v
|
||||
|
||||
migrations-lint:
|
||||
name: Validate migrations
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch master branch
|
||||
run: git fetch origin master
|
||||
|
||||
- name: Validate migration timestamps
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: ./.github/workflows/validate-migrations.sh
|
||||
|
||||
check-push-enabled:
|
||||
name: Check Docker configuration
|
||||
@ -172,7 +189,8 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
needs: [js, go, go-lint, i18n-lint, git-version, check-push-enabled]
|
||||
needs: [js, go, go-lint, i18n-lint, migrations-lint, git-version, check-push-enabled]
|
||||
if: ${{ !failure() && !cancelled() }}
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [ linux/amd64, linux/arm64, linux/arm/v5, linux/arm/v6, linux/arm/v7, linux/386, darwin/amd64, darwin/arm64, windows/amd64, windows/386 ]
|
||||
|
||||
103
.github/workflows/validate-migrations.sh
vendored
Executable file
103
.github/workflows/validate-migrations.sh
vendored
Executable file
@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# Validates that new migrations in a PR have timestamps greater than
|
||||
# the latest migration timestamp on the master branch.
|
||||
#
|
||||
# This prevents migration ordering conflicts when multiple PRs add migrations.
|
||||
# Modified existing migrations only produce warnings, not errors.
|
||||
|
||||
set -e
|
||||
|
||||
# Get the latest migration timestamp from master branch
|
||||
# Filter for files matching the pattern: 14-digit timestamp followed by _ and ending in .sql or .go
|
||||
MASTER_MIGRATIONS=$(git ls-tree --name-only origin/master -- db/migrations/ | grep -E '^db/migrations/[0-9]{14}_.*\.(sql|go)$' || true)
|
||||
|
||||
if [ -z "$MASTER_MIGRATIONS" ]; then
|
||||
echo "No migrations found on master branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MASTER_LATEST=$(echo "$MASTER_MIGRATIONS" | sed 's|db/migrations/||' | cut -c1-14 | sort -n | tail -1)
|
||||
|
||||
# Get NEW migrations (added in this PR)
|
||||
NEW_MIGRATIONS=$(git diff --name-only --diff-filter=A origin/master -- db/migrations/ | grep -E '^db/migrations/[0-9]{14}_.*\.(sql|go)$' || true)
|
||||
|
||||
# Get MODIFIED migrations (existing files that were changed)
|
||||
MODIFIED_MIGRATIONS=$(git diff --name-only --diff-filter=M origin/master -- db/migrations/ | grep -E '^db/migrations/[0-9]{14}_.*\.(sql|go)$' || true)
|
||||
|
||||
if [ -z "$NEW_MIGRATIONS" ] && [ -z "$MODIFIED_MIGRATIONS" ]; then
|
||||
echo "No new or modified migrations found in this PR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Latest migration on master: $MASTER_LATEST"
|
||||
|
||||
HAS_ERRORS=false
|
||||
|
||||
# Check NEW migrations - these MUST have valid timestamps (errors)
|
||||
if [ -n "$NEW_MIGRATIONS" ]; then
|
||||
echo ""
|
||||
echo "New migrations in this PR:"
|
||||
for migration in $NEW_MIGRATIONS; do
|
||||
TIMESTAMP=$(basename "$migration" | cut -c1-14)
|
||||
echo " - $migration (timestamp: $TIMESTAMP)"
|
||||
|
||||
if [ "$TIMESTAMP" -le "$MASTER_LATEST" ]; then
|
||||
echo "::error file=$migration::Migration timestamp $TIMESTAMP must be greater than latest master timestamp $MASTER_LATEST"
|
||||
HAS_ERRORS=true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check MODIFIED migrations - only warn, don't fail
|
||||
if [ -n "$MODIFIED_MIGRATIONS" ]; then
|
||||
echo ""
|
||||
echo "Modified existing migrations in this PR:"
|
||||
for migration in $MODIFIED_MIGRATIONS; do
|
||||
TIMESTAMP=$(basename "$migration" | cut -c1-14)
|
||||
echo " - $migration (timestamp: $TIMESTAMP)"
|
||||
echo "::warning file=$migration::Modifying existing migration files may cause issues for users who have already applied them"
|
||||
done
|
||||
|
||||
# Post a PR review comment if running in GitHub Actions with a PR
|
||||
if [ -n "$GITHUB_TOKEN" ] && [ -n "$GITHUB_REPOSITORY" ] && [ -n "$PR_NUMBER" ]; then
|
||||
# Check if a warning comment already exists to avoid duplicates
|
||||
EXISTING_COMMENT=$(curl -s \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
|
||||
| jq -r '.[] | select(.body | startswith("### ⚠️ Modified Migration Files Detected")) | .id' | head -1)
|
||||
|
||||
if [ -n "$EXISTING_COMMENT" ]; then
|
||||
echo "Warning comment already exists (comment ID: $EXISTING_COMMENT), skipping"
|
||||
else
|
||||
COMMENT_BODY="### ⚠️ Modified Migration Files Detected
|
||||
|
||||
This PR modifies existing migration files that may have already been applied by users:
|
||||
|
||||
$(for m in $MODIFIED_MIGRATIONS; do echo "- \`$m\`"; done)
|
||||
|
||||
**Warning:** Modifying migrations that have already been applied can cause issues for existing users. Please ensure this change is intentional and consider the impact on users who have already run these migrations."
|
||||
|
||||
# Use GitHub API to post a PR comment
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
|
||||
-d "$(jq -n --arg body "$COMMENT_BODY" '{body: $body}')" > /dev/null
|
||||
|
||||
echo "Posted PR comment about modified migrations"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$HAS_ERRORS" = "true" ]; then
|
||||
echo ""
|
||||
echo "ERROR: One or more NEW migrations have timestamps that are not after the latest migration on master."
|
||||
echo "Please regenerate the migration with a newer timestamp using:"
|
||||
echo " make migration-sql name=your_migration_name"
|
||||
echo " make migration-go name=your_migration_name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "All migration timestamps are valid!"
|
||||
@ -130,58 +130,99 @@ func (s *Stream) EstimatedContentLength() int {
|
||||
return int(s.mf.Duration * float32(s.bitRate) / 8 * 1024)
|
||||
}
|
||||
|
||||
// TODO This function deserves some love (refactoring)
|
||||
func selectTranscodingOptions(ctx context.Context, ds model.DataStore, mf *model.MediaFile, reqFormat string, reqBitRate int) (format string, bitRate int) {
|
||||
// Default case
|
||||
format = "raw"
|
||||
bitRate = 0
|
||||
|
||||
// If the client explicitly requests "raw"
|
||||
// then always serve the original
|
||||
if reqFormat == "raw" {
|
||||
return format, bitRate
|
||||
}
|
||||
|
||||
// If requested format matches the file’s suffix and
|
||||
// no bitrate reduction is requested then
|
||||
// stream the file without transcoding
|
||||
if reqFormat == mf.Suffix && reqBitRate == 0 {
|
||||
return format, mf.BitRate
|
||||
}
|
||||
|
||||
targetFormat, targetBitRate := findTargetTranscodingOptions(ctx, mf, reqFormat, reqBitRate)
|
||||
|
||||
// If nothing was found then stream raw
|
||||
if targetFormat == "" && targetBitRate == 0 {
|
||||
return format, 0
|
||||
}
|
||||
if reqFormat == mf.Suffix && reqBitRate == 0 {
|
||||
bitRate = mf.BitRate
|
||||
return format, bitRate
|
||||
|
||||
t, err := ds.Transcoding(ctx).FindByFormat(targetFormat)
|
||||
if err != nil {
|
||||
// TODO: log error?
|
||||
return format, 0
|
||||
}
|
||||
trc, hasDefault := request.TranscodingFrom(ctx)
|
||||
var cFormat string
|
||||
var cBitRate int
|
||||
if reqFormat != "" {
|
||||
cFormat = reqFormat
|
||||
|
||||
format = t.TargetFormat
|
||||
|
||||
// If no target bitrate was specified
|
||||
// fall back to the transcoding’s configuration
|
||||
// default bitrate
|
||||
if targetBitRate == 0 {
|
||||
bitRate = t.DefaultBitRate
|
||||
} else {
|
||||
if hasDefault {
|
||||
cFormat = trc.TargetFormat
|
||||
cBitRate = trc.DefaultBitRate
|
||||
if p, ok := request.PlayerFrom(ctx); ok {
|
||||
cBitRate = p.MaxBitRate
|
||||
}
|
||||
} else if reqBitRate > 0 && reqBitRate < mf.BitRate && conf.Server.DefaultDownsamplingFormat != "" {
|
||||
// If no format is specified and no transcoding associated to the player, but a bitrate is specified,
|
||||
// and there is no transcoding set for the player, we use the default downsampling format.
|
||||
// But only if the requested bitRate is lower than the original bitRate.
|
||||
log.Debug("Default Downsampling", "Using default downsampling format", conf.Server.DefaultDownsamplingFormat)
|
||||
cFormat = conf.Server.DefaultDownsamplingFormat
|
||||
}
|
||||
}
|
||||
if reqBitRate > 0 {
|
||||
cBitRate = reqBitRate
|
||||
}
|
||||
if cBitRate == 0 && cFormat == "" {
|
||||
return format, bitRate
|
||||
}
|
||||
t, err := ds.Transcoding(ctx).FindByFormat(cFormat)
|
||||
if err == nil {
|
||||
format = t.TargetFormat
|
||||
if cBitRate != 0 {
|
||||
bitRate = cBitRate
|
||||
} else {
|
||||
bitRate = t.DefaultBitRate
|
||||
}
|
||||
bitRate = targetBitRate
|
||||
}
|
||||
|
||||
// If the final format is the same as the original
|
||||
// and does not reduce bitrate
|
||||
// there’s no reason to transcode
|
||||
if format == mf.Suffix && bitRate >= mf.BitRate {
|
||||
format = "raw"
|
||||
bitRate = 0
|
||||
return "raw", 0
|
||||
}
|
||||
|
||||
return format, bitRate
|
||||
}
|
||||
|
||||
func findTargetTranscodingOptions(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int) (string, int) {
|
||||
// If a format is requested use that
|
||||
if reqFormat != "" {
|
||||
return reqFormat, reqBitRate
|
||||
}
|
||||
|
||||
// If a default transcoding configuration exists for this context
|
||||
if trc, ok := request.TranscodingFrom(ctx); ok {
|
||||
targetFormat := trc.TargetFormat
|
||||
targetBitRate := trc.DefaultBitRate
|
||||
|
||||
// If a player is configured adjust bitrate based on
|
||||
// user request or player limits
|
||||
if p, hasPlayer := request.PlayerFrom(ctx); hasPlayer {
|
||||
if reqBitRate > 0 {
|
||||
targetBitRate = reqBitRate
|
||||
} else if p.MaxBitRate > 0 {
|
||||
targetBitRate = p.MaxBitRate
|
||||
}
|
||||
} else if reqBitRate > 0 {
|
||||
targetBitRate = reqBitRate
|
||||
}
|
||||
|
||||
return targetFormat, targetBitRate
|
||||
}
|
||||
|
||||
// Use the default downsampling format the server is configured to but
|
||||
// only if the requested bitrate is reduced
|
||||
isBitrateReduced := reqBitRate > 0 && reqBitRate < mf.BitRate
|
||||
hasDefaultDownsamplingFormat := conf.Server.DefaultDownsamplingFormat != ""
|
||||
|
||||
if isBitrateReduced && hasDefaultDownsamplingFormat {
|
||||
log.Debug("Default Downsampling",
|
||||
"Using default downsampling format",
|
||||
conf.Server.DefaultDownsamplingFormat)
|
||||
return conf.Server.DefaultDownsamplingFormat, reqBitRate
|
||||
}
|
||||
|
||||
return "", 0
|
||||
}
|
||||
|
||||
var (
|
||||
onceTranscodingCache sync.Once
|
||||
instanceTranscodingCache TranscodingCache
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user