mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 20:47:35 +03:00
Merge branch 'master' into Add-support-for-playlist-cover-art/406
This commit is contained in:
commit
67162326a9
48 changed files with 2047 additions and 1285 deletions
|
@ -137,18 +137,18 @@ func startServer(ctx context.Context) func() error {
|
|||
// schedulePeriodicScan schedules a periodic scan of the music library, if configured.
|
||||
func schedulePeriodicScan(ctx context.Context) func() error {
|
||||
return func() error {
|
||||
schedule := conf.Server.ScanSchedule
|
||||
schedule := conf.Server.Scanner.Schedule
|
||||
if schedule == "" {
|
||||
log.Warn(ctx, "Periodic scan is DISABLED")
|
||||
return nil
|
||||
}
|
||||
|
||||
scanner := CreateScanner(ctx)
|
||||
s := CreateScanner(ctx)
|
||||
schedulerInstance := scheduler.GetInstance()
|
||||
|
||||
log.Info("Scheduling periodic scan", "schedule", schedule)
|
||||
err := schedulerInstance.Add(schedule, func() {
|
||||
_, err := scanner.ScanAll(ctx, false)
|
||||
_, err := s.ScanAll(ctx, false)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error executing periodic scan", err)
|
||||
}
|
||||
|
|
|
@ -29,8 +29,6 @@ type configOptions struct {
|
|||
DbPath string
|
||||
LogLevel string
|
||||
LogFile string
|
||||
ScanInterval time.Duration
|
||||
ScanSchedule string
|
||||
SessionTimeout time.Duration
|
||||
BaseURL string
|
||||
BasePath string
|
||||
|
@ -129,11 +127,11 @@ type configOptions struct {
|
|||
|
||||
type scannerOptions struct {
|
||||
Enabled bool
|
||||
Schedule string
|
||||
WatcherWait time.Duration
|
||||
ScanOnStartup bool
|
||||
Extractor string // Deprecated: BFR Remove before release?
|
||||
GenreSeparators string // Deprecated: BFR Update docs
|
||||
GroupAlbumReleases bool // Deprecated: BFR Update docs
|
||||
Extractor string
|
||||
GroupAlbumReleases bool // Deprecated: BFR Update docs
|
||||
}
|
||||
|
||||
type TagConf struct {
|
||||
|
@ -305,7 +303,10 @@ func Load(noConfigDump bool) {
|
|||
}
|
||||
|
||||
// BFR Remove before release
|
||||
Server.Scanner.Extractor = consts.DefaultScannerExtractor
|
||||
if Server.Scanner.Extractor != consts.DefaultScannerExtractor {
|
||||
log.Warn(fmt.Sprintf("Extractor '%s' is not implemented, using 'taglib'", Server.Scanner.Extractor))
|
||||
Server.Scanner.Extractor = consts.DefaultScannerExtractor
|
||||
}
|
||||
|
||||
// Call init hooks
|
||||
for _, hook := range hooks {
|
||||
|
@ -362,25 +363,12 @@ func validatePlaylistsPath() error {
|
|||
}
|
||||
|
||||
func validateScanSchedule() error {
|
||||
if Server.ScanInterval != -1 {
|
||||
log.Warn("ScanInterval is DEPRECATED. Please use ScanSchedule. See docs at https://navidrome.org/docs/usage/configuration-options/")
|
||||
if Server.ScanSchedule != "@every 1m" {
|
||||
log.Error("You cannot specify both ScanInterval and ScanSchedule, ignoring ScanInterval")
|
||||
} else {
|
||||
if Server.ScanInterval == 0 {
|
||||
Server.ScanSchedule = ""
|
||||
} else {
|
||||
Server.ScanSchedule = fmt.Sprintf("@every %s", Server.ScanInterval)
|
||||
}
|
||||
log.Warn("Setting ScanSchedule", "schedule", Server.ScanSchedule)
|
||||
}
|
||||
}
|
||||
if Server.ScanSchedule == "0" || Server.ScanSchedule == "" {
|
||||
Server.ScanSchedule = ""
|
||||
if Server.Scanner.Schedule == "0" || Server.Scanner.Schedule == "" {
|
||||
Server.Scanner.Schedule = ""
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
Server.ScanSchedule, err = validateSchedule(Server.ScanSchedule, "ScanSchedule")
|
||||
Server.Scanner.Schedule, err = validateSchedule(Server.Scanner.Schedule, "Scanner.Schedule")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -389,10 +377,8 @@ func validateBackupSchedule() error {
|
|||
Server.Backup.Schedule = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
Server.Backup.Schedule, err = validateSchedule(Server.Backup.Schedule, "BackupSchedule")
|
||||
|
||||
Server.Backup.Schedule, err = validateSchedule(Server.Backup.Schedule, "Backup.Schedule")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -403,7 +389,7 @@ func validateSchedule(schedule, field string) (string, error) {
|
|||
c := cron.New()
|
||||
id, err := c.AddFunc(schedule, func() {})
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Invalid %s. Please read format spec at https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format", field), "schedule", field, err)
|
||||
log.Error(fmt.Sprintf("Invalid %s. Please read format spec at https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format", field), "schedule", schedule, err)
|
||||
} else {
|
||||
c.Remove(id)
|
||||
}
|
||||
|
@ -425,8 +411,6 @@ func init() {
|
|||
viper.SetDefault("port", 4533)
|
||||
viper.SetDefault("unixsocketperm", "0660")
|
||||
viper.SetDefault("sessiontimeout", consts.DefaultSessionTimeout)
|
||||
viper.SetDefault("scaninterval", -1)
|
||||
viper.SetDefault("scanschedule", "0")
|
||||
viper.SetDefault("baseurl", "")
|
||||
viper.SetDefault("tlscert", "")
|
||||
viper.SetDefault("tlskey", "")
|
||||
|
@ -493,8 +477,8 @@ func init() {
|
|||
viper.SetDefault("jukebox.adminonly", true)
|
||||
|
||||
viper.SetDefault("scanner.enabled", true)
|
||||
viper.SetDefault("scanner.schedule", "0")
|
||||
viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor)
|
||||
viper.SetDefault("scanner.genreseparators", ";/,")
|
||||
viper.SetDefault("scanner.groupalbumreleases", false)
|
||||
viper.SetDefault("scanner.watcherwait", consts.DefaultWatcherWait)
|
||||
viper.SetDefault("scanner.scanonstartup", true)
|
||||
|
|
|
@ -187,7 +187,6 @@ var staticData = sync.OnceValue(func() insights.Data {
|
|||
data.Config.EnablePrometheus = conf.Server.Prometheus.Enabled
|
||||
data.Config.TranscodingCacheSize = conf.Server.TranscodingCacheSize
|
||||
data.Config.ImageCacheSize = conf.Server.ImageCacheSize
|
||||
data.Config.ScanSchedule = conf.Server.ScanSchedule
|
||||
data.Config.SessionTimeout = uint64(math.Trunc(conf.Server.SessionTimeout.Seconds()))
|
||||
data.Config.SearchFullString = conf.Server.SearchFullString
|
||||
data.Config.RecentlyAddedByModTime = conf.Server.RecentlyAddedByModTime
|
||||
|
@ -195,6 +194,10 @@ var staticData = sync.OnceValue(func() insights.Data {
|
|||
data.Config.BackupSchedule = conf.Server.Backup.Schedule
|
||||
data.Config.BackupCount = conf.Server.Backup.Count
|
||||
data.Config.DevActivityPanel = conf.Server.DevActivityPanel
|
||||
data.Config.ScannerEnabled = conf.Server.Scanner.Enabled
|
||||
data.Config.ScanSchedule = conf.Server.Scanner.Schedule
|
||||
data.Config.ScanWatcherWait = uint64(math.Trunc(conf.Server.Scanner.WatcherWait.Seconds()))
|
||||
data.Config.ScanOnStartup = conf.Server.Scanner.ScanOnStartup
|
||||
|
||||
return data
|
||||
})
|
||||
|
|
|
@ -43,7 +43,10 @@ type Data struct {
|
|||
LogLevel string `json:"logLevel,omitempty"`
|
||||
LogFileConfigured bool `json:"logFileConfigured,omitempty"`
|
||||
TLSConfigured bool `json:"tlsConfigured,omitempty"`
|
||||
ScannerEnabled bool `json:"scannerEnabled,omitempty"`
|
||||
ScanSchedule string `json:"scanSchedule,omitempty"`
|
||||
ScanWatcherWait uint64 `json:"scanWatcherWait,omitempty"`
|
||||
ScanOnStartup bool `json:"scanOnStartup,omitempty"`
|
||||
TranscodingCacheSize string `json:"transcodingCacheSize,omitempty"`
|
||||
ImageCacheSize string `json:"imageCacheSize,omitempty"`
|
||||
EnableArtworkPrecache bool `json:"enableArtworkPrecache,omitempty"`
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -188,20 +189,14 @@ func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, folder *m
|
|||
if !model.IsAudioFile(line) {
|
||||
continue
|
||||
}
|
||||
line = filepath.Clean(line)
|
||||
if folder != nil && !filepath.IsAbs(line) {
|
||||
line = filepath.Join(folder.AbsolutePath(), line)
|
||||
var err error
|
||||
line, err = filepath.Rel(folder.LibraryPath, line)
|
||||
if err != nil {
|
||||
log.Trace(ctx, "Error getting relative path", "playlist", pls.Name, "path", line, "folder", folder, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
filteredLines = append(filteredLines, line)
|
||||
}
|
||||
filteredLines = slice.Map(filteredLines, filepath.ToSlash)
|
||||
found, err := mediaFileRepository.FindByPaths(filteredLines)
|
||||
paths, err := s.normalizePaths(ctx, pls, folder, filteredLines)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error normalizing paths in playlist", "playlist", pls.Name, err)
|
||||
continue
|
||||
}
|
||||
found, err := mediaFileRepository.FindByPaths(paths)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error reading files from DB", "playlist", pls.Name, err)
|
||||
continue
|
||||
|
@ -210,7 +205,7 @@ func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, folder *m
|
|||
for idx := range found {
|
||||
existing[strings.ToLower(found[idx].Path)] = idx
|
||||
}
|
||||
for _, path := range filteredLines {
|
||||
for _, path := range paths {
|
||||
idx, ok := existing[strings.ToLower(path)]
|
||||
if ok {
|
||||
mfs = append(mfs, found[idx])
|
||||
|
@ -228,6 +223,64 @@ func (s *playlists) parseM3U(ctx context.Context, pls *model.Playlist, folder *m
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO This won't work for multiple libraries
|
||||
func (s *playlists) normalizePaths(ctx context.Context, pls *model.Playlist, folder *model.Folder, lines []string) ([]string, error) {
|
||||
libRegex, err := s.compileLibraryPaths(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]string, 0, len(lines))
|
||||
for idx, line := range lines {
|
||||
var libPath string
|
||||
var filePath string
|
||||
|
||||
if folder != nil && !filepath.IsAbs(line) {
|
||||
libPath = folder.LibraryPath
|
||||
filePath = filepath.Join(folder.AbsolutePath(), line)
|
||||
} else {
|
||||
cleanLine := filepath.Clean(line)
|
||||
if libPath = libRegex.FindString(cleanLine); libPath != "" {
|
||||
filePath = cleanLine
|
||||
}
|
||||
}
|
||||
|
||||
if libPath != "" {
|
||||
if rel, err := filepath.Rel(libPath, filePath); err == nil {
|
||||
res = append(res, rel)
|
||||
} else {
|
||||
log.Debug(ctx, "Error getting relative path", "playlist", pls.Name, "path", line, "libPath", libPath,
|
||||
"filePath", filePath, err)
|
||||
}
|
||||
} else {
|
||||
log.Warn(ctx, "Path in playlist not found in any library", "path", line, "line", idx)
|
||||
}
|
||||
}
|
||||
return slice.Map(res, filepath.ToSlash), nil
|
||||
}
|
||||
|
||||
func (s *playlists) compileLibraryPaths(ctx context.Context) (*regexp.Regexp, error) {
|
||||
libs, err := s.ds.Library(ctx).GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create regex patterns for each library path
|
||||
patterns := make([]string, len(libs))
|
||||
for i, lib := range libs {
|
||||
cleanPath := filepath.Clean(lib.Path)
|
||||
escapedPath := regexp.QuoteMeta(cleanPath)
|
||||
patterns[i] = fmt.Sprintf("^%s(?:/|$)", escapedPath)
|
||||
}
|
||||
// Combine all patterns into a single regex
|
||||
combinedPattern := strings.Join(patterns, "|")
|
||||
re, err := regexp.Compile(combinedPattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compiling library paths `%s`: %w", combinedPattern, err)
|
||||
}
|
||||
return re, nil
|
||||
}
|
||||
|
||||
func (s *playlists) updatePlaylist(ctx context.Context, newPls *model.Playlist) error {
|
||||
owner, _ := request.UserFrom(ctx)
|
||||
|
||||
|
@ -262,7 +315,7 @@ func (s *playlists) Update(ctx context.Context, playlistID string,
|
|||
needsInfoUpdate := name != nil || comment != nil || public != nil
|
||||
needsTrackRefresh := len(idxToRemove) > 0
|
||||
|
||||
return s.ds.WithTx(func(tx model.DataStore) error {
|
||||
return s.ds.WithTxImmediate(func(tx model.DataStore) error {
|
||||
var pls *model.Playlist
|
||||
var err error
|
||||
repo := tx.Playlist(ctx)
|
||||
|
|
|
@ -20,15 +20,20 @@ import (
|
|||
var _ = Describe("Playlists", func() {
|
||||
var ds *tests.MockDataStore
|
||||
var ps Playlists
|
||||
var mp mockedPlaylist
|
||||
var mockPlsRepo mockedPlaylistRepo
|
||||
var mockLibRepo *tests.MockLibraryRepo
|
||||
ctx := context.Background()
|
||||
|
||||
BeforeEach(func() {
|
||||
mp = mockedPlaylist{}
|
||||
mockPlsRepo = mockedPlaylistRepo{}
|
||||
mockLibRepo = &tests.MockLibraryRepo{}
|
||||
ds = &tests.MockDataStore{
|
||||
MockedPlaylist: &mp,
|
||||
MockedPlaylist: &mockPlsRepo,
|
||||
MockedLibrary: mockLibRepo,
|
||||
}
|
||||
ctx = request.WithUser(ctx, model.User{ID: "123"})
|
||||
// Path should be libPath, but we want to match the root folder referenced in the m3u, which is `/`
|
||||
mockLibRepo.SetData([]model.Library{{ID: 1, Path: "/"}})
|
||||
})
|
||||
|
||||
Describe("ImportFile", func() {
|
||||
|
@ -48,15 +53,13 @@ var _ = Describe("Playlists", func() {
|
|||
|
||||
Describe("M3U", func() {
|
||||
It("parses well-formed playlists", func() {
|
||||
// get absolute path for "tests/fixtures" folder
|
||||
pls, err := ps.ImportFile(ctx, folder, "pls1.m3u")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.OwnerID).To(Equal("123"))
|
||||
Expect(pls.Tracks).To(HaveLen(3))
|
||||
Expect(pls.Tracks).To(HaveLen(2))
|
||||
Expect(pls.Tracks[0].Path).To(Equal("tests/fixtures/playlists/test.mp3"))
|
||||
Expect(pls.Tracks[1].Path).To(Equal("tests/fixtures/playlists/test.ogg"))
|
||||
Expect(pls.Tracks[2].Path).To(Equal("/tests/fixtures/01 Invisible (RED) Edit Version.mp3"))
|
||||
Expect(mp.last).To(Equal(pls))
|
||||
Expect(mockPlsRepo.last).To(Equal(pls))
|
||||
})
|
||||
|
||||
It("parses playlists using LF ending", func() {
|
||||
|
@ -76,7 +79,7 @@ var _ = Describe("Playlists", func() {
|
|||
It("parses well-formed playlists", func() {
|
||||
pls, err := ps.ImportFile(ctx, folder, "recently_played.nsp")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mp.last).To(Equal(pls))
|
||||
Expect(mockPlsRepo.last).To(Equal(pls))
|
||||
Expect(pls.OwnerID).To(Equal("123"))
|
||||
Expect(pls.Name).To(Equal("Recently Played"))
|
||||
Expect(pls.Comment).To(Equal("Recently played tracks"))
|
||||
|
@ -98,79 +101,90 @@ var _ = Describe("Playlists", func() {
|
|||
repo = &mockedMediaFileFromListRepo{}
|
||||
ds.MockedMediaFile = repo
|
||||
ps = NewPlaylists(ds)
|
||||
mockLibRepo.SetData([]model.Library{{ID: 1, Path: "/music"}, {ID: 2, Path: "/new"}})
|
||||
ctx = request.WithUser(ctx, model.User{ID: "123"})
|
||||
})
|
||||
|
||||
It("parses well-formed playlists", func() {
|
||||
repo.data = []string{
|
||||
"tests/fixtures/test.mp3",
|
||||
"tests/fixtures/test.ogg",
|
||||
"/tests/fixtures/01 Invisible (RED) Edit Version.mp3",
|
||||
"tests/test.mp3",
|
||||
"tests/test.ogg",
|
||||
"tests/01 Invisible (RED) Edit Version.mp3",
|
||||
"downloads/newfile.flac",
|
||||
}
|
||||
f, _ := os.Open("tests/fixtures/playlists/pls-with-name.m3u")
|
||||
defer f.Close()
|
||||
m3u := strings.Join([]string{
|
||||
"#PLAYLIST:playlist 1",
|
||||
"/music/tests/test.mp3",
|
||||
"/music/tests/test.ogg",
|
||||
"/new/downloads/newfile.flac",
|
||||
"file:///music/tests/01%20Invisible%20(RED)%20Edit%20Version.mp3",
|
||||
}, "\n")
|
||||
f := strings.NewReader(m3u)
|
||||
|
||||
pls, err := ps.ImportM3U(ctx, f)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.OwnerID).To(Equal("123"))
|
||||
Expect(pls.Name).To(Equal("playlist 1"))
|
||||
Expect(pls.Sync).To(BeFalse())
|
||||
Expect(pls.Tracks).To(HaveLen(3))
|
||||
Expect(pls.Tracks[0].Path).To(Equal("tests/fixtures/test.mp3"))
|
||||
Expect(pls.Tracks[1].Path).To(Equal("tests/fixtures/test.ogg"))
|
||||
Expect(pls.Tracks[2].Path).To(Equal("/tests/fixtures/01 Invisible (RED) Edit Version.mp3"))
|
||||
Expect(mp.last).To(Equal(pls))
|
||||
f.Close()
|
||||
|
||||
Expect(pls.Tracks).To(HaveLen(4))
|
||||
Expect(pls.Tracks[0].Path).To(Equal("tests/test.mp3"))
|
||||
Expect(pls.Tracks[1].Path).To(Equal("tests/test.ogg"))
|
||||
Expect(pls.Tracks[2].Path).To(Equal("downloads/newfile.flac"))
|
||||
Expect(pls.Tracks[3].Path).To(Equal("tests/01 Invisible (RED) Edit Version.mp3"))
|
||||
Expect(mockPlsRepo.last).To(Equal(pls))
|
||||
})
|
||||
|
||||
It("sets the playlist name as a timestamp if the #PLAYLIST directive is not present", func() {
|
||||
repo.data = []string{
|
||||
"tests/fixtures/test.mp3",
|
||||
"tests/fixtures/test.ogg",
|
||||
"/tests/fixtures/01 Invisible (RED) Edit Version.mp3",
|
||||
"tests/test.mp3",
|
||||
"tests/test.ogg",
|
||||
"/tests/01 Invisible (RED) Edit Version.mp3",
|
||||
}
|
||||
f, _ := os.Open("tests/fixtures/playlists/pls-without-name.m3u")
|
||||
defer f.Close()
|
||||
m3u := strings.Join([]string{
|
||||
"/music/tests/test.mp3",
|
||||
"/music/tests/test.ogg",
|
||||
}, "\n")
|
||||
f := strings.NewReader(m3u)
|
||||
pls, err := ps.ImportM3U(ctx, f)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = time.Parse(time.RFC3339, pls.Name)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.Tracks).To(HaveLen(3))
|
||||
Expect(pls.Tracks).To(HaveLen(2))
|
||||
})
|
||||
|
||||
It("returns only tracks that exist in the database and in the same other as the m3u", func() {
|
||||
repo.data = []string{
|
||||
"test1.mp3",
|
||||
"test2.mp3",
|
||||
"test3.mp3",
|
||||
"album1/test1.mp3",
|
||||
"album2/test2.mp3",
|
||||
"album3/test3.mp3",
|
||||
}
|
||||
m3u := strings.Join([]string{
|
||||
"test3.mp3",
|
||||
"test1.mp3",
|
||||
"test4.mp3",
|
||||
"test2.mp3",
|
||||
"/music/album3/test3.mp3",
|
||||
"/music/album1/test1.mp3",
|
||||
"/music/album4/test4.mp3",
|
||||
"/music/album2/test2.mp3",
|
||||
}, "\n")
|
||||
f := strings.NewReader(m3u)
|
||||
pls, err := ps.ImportM3U(ctx, f)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.Tracks).To(HaveLen(3))
|
||||
Expect(pls.Tracks[0].Path).To(Equal("test3.mp3"))
|
||||
Expect(pls.Tracks[1].Path).To(Equal("test1.mp3"))
|
||||
Expect(pls.Tracks[2].Path).To(Equal("test2.mp3"))
|
||||
Expect(pls.Tracks[0].Path).To(Equal("album3/test3.mp3"))
|
||||
Expect(pls.Tracks[1].Path).To(Equal("album1/test1.mp3"))
|
||||
Expect(pls.Tracks[2].Path).To(Equal("album2/test2.mp3"))
|
||||
})
|
||||
|
||||
It("is case-insensitive when comparing paths", func() {
|
||||
repo.data = []string{
|
||||
"tEsT1.Mp3",
|
||||
"abc/tEsT1.Mp3",
|
||||
}
|
||||
m3u := strings.Join([]string{
|
||||
"TeSt1.mP3",
|
||||
"/music/ABC/TeSt1.mP3",
|
||||
}, "\n")
|
||||
f := strings.NewReader(m3u)
|
||||
pls, err := ps.ImportM3U(ctx, f)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.Tracks).To(HaveLen(1))
|
||||
Expect(pls.Tracks[0].Path).To(Equal("tEsT1.Mp3"))
|
||||
Expect(pls.Tracks[0].Path).To(Equal("abc/tEsT1.Mp3"))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -254,16 +268,16 @@ func (r *mockedMediaFileFromListRepo) FindByPaths([]string) (model.MediaFiles, e
|
|||
return mfs, nil
|
||||
}
|
||||
|
||||
type mockedPlaylist struct {
|
||||
type mockedPlaylistRepo struct {
|
||||
last *model.Playlist
|
||||
model.PlaylistRepository
|
||||
}
|
||||
|
||||
func (r *mockedPlaylist) FindByPath(string) (*model.Playlist, error) {
|
||||
func (r *mockedPlaylistRepo) FindByPath(string) (*model.Playlist, error) {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (r *mockedPlaylist) Put(pls *model.Playlist) error {
|
||||
func (r *mockedPlaylistRepo) Put(pls *model.Playlist) error {
|
||||
r.last = pls
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ func (p *playTracker) Submit(ctx context.Context, submissions []Submission) erro
|
|||
success := 0
|
||||
|
||||
for _, s := range submissions {
|
||||
mf, err := p.ds.MediaFile(ctx).Get(s.TrackID)
|
||||
mf, err := p.ds.MediaFile(ctx).GetWithParticipants(s.TrackID)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Cannot find track for scrobbling", "id", s.TrackID, "user", username, err)
|
||||
continue
|
||||
|
|
|
@ -68,6 +68,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
Expect(fake.NowPlayingCalled).To(BeTrue())
|
||||
Expect(fake.UserID).To(Equal("u-1"))
|
||||
Expect(fake.Track.ID).To(Equal("123"))
|
||||
Expect(fake.Track.Participants).To(Equal(track.Participants))
|
||||
})
|
||||
It("does not send track to agent if user has not authorized", func() {
|
||||
fake.Authorized = false
|
||||
|
@ -132,6 +133,7 @@ var _ = Describe("PlayTracker", func() {
|
|||
Expect(fake.ScrobbleCalled).To(BeTrue())
|
||||
Expect(fake.UserID).To(Equal("u-1"))
|
||||
Expect(fake.LastScrobble.ID).To(Equal("123"))
|
||||
Expect(fake.LastScrobble.Participants).To(Equal(track.Participants))
|
||||
})
|
||||
|
||||
It("increments play counts in the DB", func() {
|
||||
|
@ -191,7 +193,6 @@ var _ = Describe("PlayTracker", func() {
|
|||
Expect(artist1.PlayCount).To(Equal(int64(1)))
|
||||
Expect(artist2.PlayCount).To(Equal(int64(1)))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -41,6 +41,7 @@ type DataStore interface {
|
|||
|
||||
Resource(ctx context.Context, model interface{}) ResourceRepository
|
||||
|
||||
WithTx(func(tx DataStore) error) error
|
||||
WithTx(block func(tx DataStore) error, scope ...string) error
|
||||
WithTxImmediate(block func(tx DataStore) error, scope ...string) error
|
||||
GC(ctx context.Context) error
|
||||
}
|
||||
|
|
|
@ -200,9 +200,9 @@ func init() {
|
|||
conf.AddHook(func() {
|
||||
loadTagMappings()
|
||||
|
||||
// This is here to avoid cyclic imports. The criteria package needs to know all tag names, so they can be used in
|
||||
// smart playlists
|
||||
criteria.AddTagNames(tagNames())
|
||||
// This is here to avoid cyclic imports. The criteria package needs to know all tag names, so they can be
|
||||
// used in smart playlists
|
||||
criteria.AddRoles(slices.Collect(maps.Keys(AllRoles)))
|
||||
criteria.AddTagNames(tagNames())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -150,27 +150,6 @@ func (r *mediaFileRepository) GetWithParticipants(id string) (*model.MediaFile,
|
|||
return m, err
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) getParticipants(m *model.MediaFile) (model.Participants, error) {
|
||||
ar := NewArtistRepository(r.ctx, r.db)
|
||||
ids := m.Participants.AllIDs()
|
||||
artists, err := ar.GetAll(model.QueryOptions{Filters: Eq{"id": ids}})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting participants: %w", err)
|
||||
}
|
||||
artistMap := slice.ToMap(artists, func(a model.Artist) (string, model.Artist) {
|
||||
return a.ID, a
|
||||
})
|
||||
p := m.Participants
|
||||
for role, artistList := range p {
|
||||
for idx, artist := range artistList {
|
||||
if a, ok := artistMap[artist.ID]; ok {
|
||||
p[role][idx].Artist = a
|
||||
}
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (r *mediaFileRepository) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||
sq := r.selectMediaFile(options...)
|
||||
var res dbMediaFiles
|
||||
|
|
|
@ -118,17 +118,45 @@ func (s *SQLStore) Resource(ctx context.Context, m interface{}) model.ResourceRe
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) WithTx(block func(tx model.DataStore) error) error {
|
||||
conn, ok := s.db.(*dbx.DB)
|
||||
if !ok {
|
||||
func (s *SQLStore) WithTx(block func(tx model.DataStore) error, scope ...string) error {
|
||||
var msg string
|
||||
if len(scope) > 0 {
|
||||
msg = scope[0]
|
||||
}
|
||||
start := time.Now()
|
||||
conn, inTx := s.db.(*dbx.DB)
|
||||
if !inTx {
|
||||
log.Trace("Nested Transaction started", "scope", msg)
|
||||
conn = dbx.NewFromDB(db.Db(), db.Driver)
|
||||
} else {
|
||||
log.Trace("Transaction started", "scope", msg)
|
||||
}
|
||||
return conn.Transactional(func(tx *dbx.Tx) error {
|
||||
newDb := &SQLStore{db: tx}
|
||||
return block(newDb)
|
||||
err := block(newDb)
|
||||
if !inTx {
|
||||
log.Trace("Nested Transaction finished", "scope", msg, "elapsed", time.Since(start), err)
|
||||
} else {
|
||||
log.Trace("Transaction finished", "scope", msg, "elapsed", time.Since(start), err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SQLStore) WithTxImmediate(block func(tx model.DataStore) error, scope ...string) error {
|
||||
ctx := context.Background()
|
||||
return s.WithTx(func(tx model.DataStore) error {
|
||||
// Workaround to force the transaction to be upgraded to immediate mode to avoid deadlocks
|
||||
// See https://berthub.eu/articles/posts/a-brief-post-on-sqlite3-database-locked-despite-timeout/
|
||||
_ = tx.Property(ctx).Put("tmp_lock_flag", "")
|
||||
defer func() {
|
||||
_ = tx.Property(ctx).Delete("tmp_lock_flag")
|
||||
}()
|
||||
|
||||
return block(tx)
|
||||
}, scope...)
|
||||
}
|
||||
|
||||
func (s *SQLStore) GC(ctx context.Context) error {
|
||||
trace := func(ctx context.Context, msg string, f func() error) func() error {
|
||||
return func() error {
|
||||
|
|
|
@ -40,7 +40,11 @@ func mf(mf model.MediaFile) model.MediaFile {
|
|||
mf.Tags = model.Tags{}
|
||||
mf.LibraryID = 1
|
||||
mf.LibraryPath = "music" // Default folder
|
||||
mf.Participants = model.Participants{}
|
||||
mf.Participants = model.Participants{
|
||||
model.RoleArtist: model.ParticipantList{
|
||||
model.Participant{Artist: model.Artist{ID: mf.ArtistID, Name: mf.Artist}},
|
||||
},
|
||||
}
|
||||
return mf
|
||||
}
|
||||
|
||||
|
@ -135,14 +139,6 @@ var _ = BeforeSuite(func() {
|
|||
// }
|
||||
//}
|
||||
|
||||
mr := NewMediaFileRepository(ctx, conn)
|
||||
for i := range testSongs {
|
||||
err := mr.Put(&testSongs[i])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
alr := NewAlbumRepository(ctx, conn).(*albumRepository)
|
||||
for i := range testAlbums {
|
||||
a := testAlbums[i]
|
||||
|
@ -161,6 +157,14 @@ var _ = BeforeSuite(func() {
|
|||
}
|
||||
}
|
||||
|
||||
mr := NewMediaFileRepository(ctx, conn)
|
||||
for i := range testSongs {
|
||||
err := mr.Put(&testSongs[i])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
rar := NewRadioRepository(ctx, conn)
|
||||
for i := range testRadios {
|
||||
r := testRadios[i]
|
||||
|
|
|
@ -82,6 +82,10 @@ func (r *scrobbleBufferRepository) Next(service string, userId string) (*model.S
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.ScrobbleEntry.Participants, err = r.getParticipants(&res.ScrobbleEntry.MediaFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.ScrobbleEntry, nil
|
||||
}
|
||||
|
||||
|
|
208
persistence/scrobble_buffer_repository_test.go
Normal file
208
persistence/scrobble_buffer_repository_test.go
Normal file
|
@ -0,0 +1,208 @@
|
|||
package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/id"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("ScrobbleBufferRepository", func() {
|
||||
var scrobble model.ScrobbleBufferRepository
|
||||
var rawRepo sqlRepository
|
||||
|
||||
enqueueTime := time.Date(2025, 01, 01, 00, 00, 00, 00, time.Local)
|
||||
var ids []string
|
||||
|
||||
var insertManually = func(service, userId, mediaFileId string, playTime time.Time) {
|
||||
id := id.NewRandom()
|
||||
ids = append(ids, id)
|
||||
|
||||
ins := squirrel.Insert("scrobble_buffer").SetMap(map[string]interface{}{
|
||||
"id": id,
|
||||
"user_id": userId,
|
||||
"service": service,
|
||||
"media_file_id": mediaFileId,
|
||||
"play_time": playTime,
|
||||
"enqueue_time": enqueueTime,
|
||||
})
|
||||
_, err := rawRepo.executeSQL(ins)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx := request.WithUser(log.NewContext(context.TODO()), model.User{ID: "userid", UserName: "johndoe", IsAdmin: true})
|
||||
db := GetDBXBuilder()
|
||||
scrobble = NewScrobbleBufferRepository(ctx, db)
|
||||
|
||||
rawRepo = sqlRepository{
|
||||
ctx: ctx,
|
||||
tableName: "scrobble_buffer",
|
||||
db: db,
|
||||
}
|
||||
ids = []string{}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
del := squirrel.Delete(rawRepo.tableName)
|
||||
_, err := rawRepo.executeSQL(del)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Describe("Without data", func() {
|
||||
Describe("Count", func() {
|
||||
It("returns zero when empty", func() {
|
||||
count, err := scrobble.Length()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(BeZero())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Dequeue", func() {
|
||||
It("is a no-op when deleting a nonexistent item", func() {
|
||||
err := scrobble.Dequeue(&model.ScrobbleEntry{ID: "fake"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
count, err := scrobble.Length()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(Equal(int64(0)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Next", func() {
|
||||
It("should not fail with no item for the service", func() {
|
||||
entry, err := scrobble.Next("fake", "userid")
|
||||
Expect(entry).To(BeNil())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("UserIds", func() {
|
||||
It("should return empty list with no data", func() {
|
||||
ids, err := scrobble.UserIDs("service")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ids).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("With data", func() {
|
||||
timeA := enqueueTime.Add(24 * time.Hour)
|
||||
timeB := enqueueTime.Add(48 * time.Hour)
|
||||
timeC := enqueueTime.Add(72 * time.Hour)
|
||||
timeD := enqueueTime.Add(96 * time.Hour)
|
||||
|
||||
BeforeEach(func() {
|
||||
insertManually("a", "userid", "1001", timeB)
|
||||
insertManually("a", "userid", "1002", timeA)
|
||||
insertManually("a", "2222", "1003", timeC)
|
||||
insertManually("b", "2222", "1004", timeD)
|
||||
})
|
||||
|
||||
Describe("Count", func() {
|
||||
It("Returns count when populated", func() {
|
||||
count, err := scrobble.Length()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(Equal(int64(4)))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Dequeue", func() {
|
||||
It("is a no-op when deleting a nonexistent item", func() {
|
||||
err := scrobble.Dequeue(&model.ScrobbleEntry{ID: "fake"})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
count, err := scrobble.Length()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(Equal(int64(4)))
|
||||
})
|
||||
|
||||
It("deletes an item when specified properly", func() {
|
||||
err := scrobble.Dequeue(&model.ScrobbleEntry{ID: ids[3]})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
count, err := scrobble.Length()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(Equal(int64(3)))
|
||||
|
||||
entry, err := scrobble.Next("b", "2222")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(entry).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Enqueue", func() {
|
||||
DescribeTable("enqueues an item properly",
|
||||
func(service, userId, fileId string, playTime time.Time) {
|
||||
now := time.Now()
|
||||
err := scrobble.Enqueue(service, userId, fileId, playTime)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
count, err := scrobble.Length()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(count).To(Equal(int64(5)))
|
||||
|
||||
entry, err := scrobble.Next(service, userId)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(entry).ToNot(BeNil())
|
||||
|
||||
Expect(entry.EnqueueTime).To(BeTemporally("~", now))
|
||||
Expect(entry.MediaFileID).To(Equal(fileId))
|
||||
Expect(entry.PlayTime).To(BeTemporally("==", playTime))
|
||||
},
|
||||
Entry("to an existing service with multiple values", "a", "userid", "1004", enqueueTime),
|
||||
Entry("to a new service", "c", "2222", "1001", timeD),
|
||||
Entry("to an existing service as new user", "b", "userid", "1003", timeC),
|
||||
)
|
||||
})
|
||||
|
||||
Describe("Next", func() {
|
||||
DescribeTable("Returns the next item when populated",
|
||||
func(service, id string, playTime time.Time, fileId, artistId string) {
|
||||
entry, err := scrobble.Next(service, id)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(entry).ToNot(BeNil())
|
||||
|
||||
Expect(entry.Service).To(Equal(service))
|
||||
Expect(entry.UserID).To(Equal(id))
|
||||
Expect(entry.PlayTime).To(BeTemporally("==", playTime))
|
||||
Expect(entry.EnqueueTime).To(BeTemporally("==", enqueueTime))
|
||||
Expect(entry.MediaFileID).To(Equal(fileId))
|
||||
|
||||
Expect(entry.MediaFile.Participants).To(HaveLen(1))
|
||||
|
||||
artists, ok := entry.MediaFile.Participants[model.RoleArtist]
|
||||
Expect(ok).To(BeTrue(), "no artist role in participants")
|
||||
|
||||
Expect(artists).To(HaveLen(1))
|
||||
Expect(artists[0].ID).To(Equal(artistId))
|
||||
},
|
||||
|
||||
Entry("Service with multiple values for one user", "a", "userid", timeA, "1002", "3"),
|
||||
Entry("Service with users", "a", "2222", timeC, "1003", "2"),
|
||||
Entry("Service with one user", "b", "2222", timeD, "1004", "2"),
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
Describe("UserIds", func() {
|
||||
It("should return ordered list for services", func() {
|
||||
ids, err := scrobble.UserIDs("a")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ids).To(Equal([]string{"2222", "userid"}))
|
||||
})
|
||||
|
||||
It("should return for a different service", func() {
|
||||
ids, err := scrobble.UserIDs("b")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(ids).To(Equal([]string{"2222"}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -64,3 +64,24 @@ func (r sqlRepository) updateParticipants(itemID string, participants model.Part
|
|||
_, err = r.executeSQL(sqi)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *sqlRepository) getParticipants(m *model.MediaFile) (model.Participants, error) {
|
||||
ar := NewArtistRepository(r.ctx, r.db)
|
||||
ids := m.Participants.AllIDs()
|
||||
artists, err := ar.GetAll(model.QueryOptions{Filters: Eq{"id": ids}})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting participants: %w", err)
|
||||
}
|
||||
artistMap := slice.ToMap(artists, func(a model.Artist) (string, model.Artist) {
|
||||
return a.ID, a
|
||||
})
|
||||
p := m.Participants
|
||||
for role, artistList := range p {
|
||||
for idx, artist := range artistList {
|
||||
if a, ok := artistMap[artist.ID]; ok {
|
||||
p[role][idx].Artist = a
|
||||
}
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"trackNumber": "Titel #",
|
||||
"playCount": "Wiedergaben",
|
||||
"title": "Titel",
|
||||
"artist": "Künstler",
|
||||
"artist": "Interpret",
|
||||
"album": "Album",
|
||||
"path": "Dateipfad",
|
||||
"genre": "Genre",
|
||||
|
@ -26,7 +26,13 @@
|
|||
"bpm": "BPM",
|
||||
"playDate": "Letzte Wiedergabe",
|
||||
"channels": "Spuren",
|
||||
"createdAt": "Hinzugefügt"
|
||||
"createdAt": "Hinzugefügt",
|
||||
"grouping": "Gruppierung",
|
||||
"mood": "Stimmung",
|
||||
"participants": "Weitere Beteiligte",
|
||||
"tags": "Weitere Tags",
|
||||
"mappedTags": "Gemappte Tags",
|
||||
"rawTags": "Tag Rohdaten"
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "Später abspielen",
|
||||
|
@ -58,7 +64,13 @@
|
|||
"originalDate": "Ursprünglich",
|
||||
"releaseDate": "Erschienen",
|
||||
"releases": "Veröffentlichung |||| Veröffentlichungen",
|
||||
"released": "Erschienen"
|
||||
"released": "Erschienen",
|
||||
"recordLabel": "Label",
|
||||
"catalogNum": "Katalognummer",
|
||||
"releaseType": "Typ",
|
||||
"grouping": "Gruppierung",
|
||||
"media": "Medium",
|
||||
"mood": "Stimmung"
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "Abspielen",
|
||||
|
@ -89,7 +101,23 @@
|
|||
"playCount": "Wiedergaben",
|
||||
"rating": "Bewertung",
|
||||
"genre": "Genre",
|
||||
"size": "Größe"
|
||||
"size": "Größe",
|
||||
"role": "Rolle"
|
||||
},
|
||||
"roles": {
|
||||
"albumartist": "Albuminterpret |||| Albuminterpreten",
|
||||
"artist": "Interpret |||| Interpreten",
|
||||
"composer": "Komponist |||| Komponisten",
|
||||
"conductor": "Dirigent |||| Dirigenten",
|
||||
"lyricist": "Texter |||| Texter",
|
||||
"arranger": "Arrangeur |||| Arrangeure",
|
||||
"producer": "Produzent |||| Produzenten",
|
||||
"director": "Direktor |||| Direktoren",
|
||||
"engineer": "Ingenieur |||| Ingenieure",
|
||||
"mixer": "Mixer |||| Mixer",
|
||||
"remixer": "Remixer |||| Remixer",
|
||||
"djmixer": "DJ Mixer |||| DJ Mixer",
|
||||
"performer": "ausübender Künstler |||| ausübende Künstler"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
|
@ -198,6 +226,20 @@
|
|||
"createdAt": "Erstellt am",
|
||||
"downloadable": "Downloads erlauben?"
|
||||
}
|
||||
},
|
||||
"missing": {
|
||||
"name": "Fehlende Datei |||| Fehlende Dateien",
|
||||
"fields": {
|
||||
"path": "Pfad",
|
||||
"size": "Größe",
|
||||
"updatedAt": "Fehlt seit"
|
||||
},
|
||||
"actions": {
|
||||
"remove": "Entfernen"
|
||||
},
|
||||
"notifications": {
|
||||
"removed": "Fehlende Datei(en) entfernt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ra": {
|
||||
|
@ -375,7 +417,9 @@
|
|||
"shareSuccess": "URL in Zwischenablage kopiert: %{url}",
|
||||
"shareFailure": "Fehler URL %{url} konnte nicht in Zwischenablage kopiert werden",
|
||||
"downloadDialogTitle": "Download %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "In Zwischenablage kopieren: Ctrl+C, Enter"
|
||||
"shareCopyToClipboard": "In Zwischenablage kopieren: Ctrl+C, Enter",
|
||||
"remove_missing_title": "Fehlende Dateien entfernen",
|
||||
"remove_missing_content": "Möchtest du die ausgewählten Fehlenden Dateien wirklich aus der Datenbank entfernen? Alle Referenzen zu den Dateien wie Anzahl Wiedergaben und Bewertungen werden permanent gelöscht."
|
||||
},
|
||||
"menu": {
|
||||
"library": "Bibliothek",
|
||||
|
@ -421,7 +465,7 @@
|
|||
"toggleMiniModeText": "Minimieren",
|
||||
"destroyText": "Zerstören",
|
||||
"downloadText": "Herunterladen",
|
||||
"removeAudioListsText": "Audiolisten löschen",
|
||||
"removeAudioListsText": "Audiolisten entfernen",
|
||||
"clickToDeleteText": "Klicken um %{name} zu Löschen",
|
||||
"emptyLyricText": "Kein Liedtext",
|
||||
"playModeText": {
|
||||
|
|
|
@ -26,7 +26,13 @@
|
|||
"bpm": "BPM",
|
||||
"playDate": "Últimas reproducciones",
|
||||
"channels": "Canales",
|
||||
"createdAt": "Creado el"
|
||||
"createdAt": "Creado el",
|
||||
"grouping": "",
|
||||
"mood": "",
|
||||
"participants": "",
|
||||
"tags": "",
|
||||
"mappedTags": "",
|
||||
"rawTags": ""
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "Reproducir después",
|
||||
|
@ -58,7 +64,13 @@
|
|||
"originalDate": "Original",
|
||||
"releaseDate": "Publicado",
|
||||
"releases": "Lanzamiento |||| Lanzamientos",
|
||||
"released": "Publicado"
|
||||
"released": "Publicado",
|
||||
"recordLabel": "",
|
||||
"catalogNum": "",
|
||||
"releaseType": "",
|
||||
"grouping": "",
|
||||
"media": "",
|
||||
"mood": ""
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "Reproducir",
|
||||
|
@ -73,8 +85,8 @@
|
|||
"lists": {
|
||||
"all": "Todos",
|
||||
"random": "Aleatorio",
|
||||
"recentlyAdded": "Añadidos recientemente",
|
||||
"recentlyPlayed": "Reproducidos recientemente",
|
||||
"recentlyAdded": "Recientes",
|
||||
"recentlyPlayed": "Recientes",
|
||||
"mostPlayed": "Más reproducidos",
|
||||
"starred": "Favoritos",
|
||||
"topRated": "Los mejores calificados"
|
||||
|
@ -89,7 +101,23 @@
|
|||
"playCount": "Reproducciones",
|
||||
"rating": "Calificación",
|
||||
"genre": "Género",
|
||||
"size": "Tamaño"
|
||||
"size": "Tamaño",
|
||||
"role": ""
|
||||
},
|
||||
"roles": {
|
||||
"albumartist": "",
|
||||
"artist": "",
|
||||
"composer": "",
|
||||
"conductor": "",
|
||||
"lyricist": "",
|
||||
"arranger": "",
|
||||
"producer": "",
|
||||
"director": "",
|
||||
"engineer": "",
|
||||
"mixer": "",
|
||||
"remixer": "",
|
||||
"djmixer": "",
|
||||
"performer": ""
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
|
@ -198,6 +226,20 @@
|
|||
"createdAt": "Creado el",
|
||||
"downloadable": "¿Permitir descargas?"
|
||||
}
|
||||
},
|
||||
"missing": {
|
||||
"name": "",
|
||||
"fields": {
|
||||
"path": "",
|
||||
"size": "",
|
||||
"updatedAt": ""
|
||||
},
|
||||
"actions": {
|
||||
"remove": ""
|
||||
},
|
||||
"notifications": {
|
||||
"removed": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"ra": {
|
||||
|
@ -375,7 +417,9 @@
|
|||
"shareSuccess": "URL copiada al portapapeles: %{url}",
|
||||
"shareFailure": "Error al copiar la URL %{url} al portapapeles",
|
||||
"downloadDialogTitle": "Descargar %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "Copiar al portapapeles: Ctrl+C, Intro"
|
||||
"shareCopyToClipboard": "Copiar al portapapeles: Ctrl+C, Intro",
|
||||
"remove_missing_title": "",
|
||||
"remove_missing_content": ""
|
||||
},
|
||||
"menu": {
|
||||
"library": "Biblioteca",
|
||||
|
@ -465,4 +509,4 @@
|
|||
"current_song": "Canción actual"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,470 +1,512 @@
|
|||
{
|
||||
"languageName": "Euskara",
|
||||
"resources": {
|
||||
"song": {
|
||||
"name": "Abestia |||| Abestiak",
|
||||
"fields": {
|
||||
"albumArtist": "Albumaren artista",
|
||||
"duration": "Iraupena",
|
||||
"trackNumber": "#",
|
||||
"playCount": "Erreprodukzioak",
|
||||
"title": "Titulua",
|
||||
"artist": "Artista",
|
||||
"album": "Albuma",
|
||||
"path": "Fitxategiaren bidea",
|
||||
"genre": "Generoa",
|
||||
"compilation": "Konpilazioa",
|
||||
"year": "Urtea",
|
||||
"size": "Fitxategiaren tamaina",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"bitRate": "Bit tasa",
|
||||
"channels": "Kanalak",
|
||||
"discSubtitle": "Diskoaren azpititulua",
|
||||
"starred": "Gogokoa",
|
||||
"comment": "Iruzkina",
|
||||
"rating": "Balorazioa",
|
||||
"quality": "Kalitatea",
|
||||
"bpm": "BPM",
|
||||
"playDate": "Azkenekoz erreproduzitua:",
|
||||
"createdAt": "Gehitu zen data:"
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "Erreproduzitu ondoren",
|
||||
"playNow": "Erreproduzitu orain",
|
||||
"addToPlaylist": "Gehitu erreprodukzio-zerrendara",
|
||||
"shuffleAll": "Erreprodukzio aleatorioa",
|
||||
"download": "Deskargatu",
|
||||
"playNext": "Hurrengoa",
|
||||
"info": "Lortu informazioa"
|
||||
}
|
||||
},
|
||||
"album": {
|
||||
"name": "Albuma |||| Albumak",
|
||||
"fields": {
|
||||
"albumArtist": "Albumaren artista",
|
||||
"artist": "Artista",
|
||||
"duration": "Iraupena",
|
||||
"songCount": "abesti",
|
||||
"playCount": "Erreprodukzioak",
|
||||
"size": "Fitxategiaren tamaina",
|
||||
"name": "Izena",
|
||||
"genre": "Generoa",
|
||||
"compilation": "Konpilazioa",
|
||||
"year": "Urtea",
|
||||
"originalDate": "Jatorrizkoa",
|
||||
"releaseDate": "Argitaratze-data:",
|
||||
"releases": "Argitaratzea |||| Argitaratzeak",
|
||||
"released": "Argitaratua",
|
||||
"updatedAt": "Aktualizatze-data:",
|
||||
"comment": "Iruzkina",
|
||||
"rating": "Balorazioa",
|
||||
"createdAt": "Gehitu zen data:"
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "Erreproduzitu",
|
||||
"playNext": "Erreproduzitu segidan",
|
||||
"addToQueue": "Erreproduzitu amaieran",
|
||||
"share": "Partekatu",
|
||||
"shuffle": "Aletorioa",
|
||||
"addToPlaylist": "Gehitu zerrendara",
|
||||
"download": "Deskargatu",
|
||||
"info": "Lortu informazioa"
|
||||
},
|
||||
"lists": {
|
||||
"all": "Guztiak",
|
||||
"random": "Aleatorioa",
|
||||
"recentlyAdded": "Berriki gehitutakoak",
|
||||
"recentlyPlayed": "Berriki entzundakoak",
|
||||
"mostPlayed": "Gehien entzundakoak",
|
||||
"starred": "Gogokoak",
|
||||
"topRated": "Hobekien baloratutakoak"
|
||||
}
|
||||
},
|
||||
"artist": {
|
||||
"name": "Artista |||| Artistak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"albumCount": "Album kopurua",
|
||||
"songCount": "Abesti kopurua",
|
||||
"size": "Tamaina",
|
||||
"playCount": "Erreprodukzio kopurua",
|
||||
"rating": "Balorazioa",
|
||||
"genre": "Generoa"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"name": "Erabiltzailea |||| Erabiltzaileak",
|
||||
"fields": {
|
||||
"userName": "Erabiltzailearen izena",
|
||||
"isAdmin": "Administratzailea da",
|
||||
"lastLoginAt": "Azken saio hasiera:",
|
||||
"lastAccessAt": "Azken sarbidea",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"name": "Izena",
|
||||
"password": "Pasahitza",
|
||||
"createdAt": "Sortze-data:",
|
||||
"changePassword": "Pasahitza aldatu?",
|
||||
"currentPassword": "Uneko pasahitza",
|
||||
"newPassword": "Pasahitz berria",
|
||||
"token": "Tokena"
|
||||
},
|
||||
"helperTexts": {
|
||||
"name": "Aldaketak saioa hasten duzun hurrengoan islatuko dira"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Erabiltzailea sortu da",
|
||||
"updated": "Erabiltzailea eguneratu da",
|
||||
"deleted": "Erabiltzailea ezabatu da"
|
||||
},
|
||||
"message": {
|
||||
"listenBrainzToken": "Idatzi zure ListenBrainz erabiltzailearen tokena",
|
||||
"clickHereForToken": "Egin klik hemen tokena lortzeko"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"name": "Erreproduktorea |||| Erreproduktoreak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"transcodingId": "Transkodifikazioa",
|
||||
"maxBitRate": "Gehienezko bit tasa",
|
||||
"client": "Bezeroa",
|
||||
"userName": "Erabiltzailea",
|
||||
"lastSeen": "Azken konexioa",
|
||||
"reportRealPath": "Erakutsi bide absolutua",
|
||||
"scrobbleEnabled": "Bidali erabiltzailearen ohiturak hirugarrenen zerbitzuetara"
|
||||
}
|
||||
},
|
||||
"transcoding": {
|
||||
"name": "Transkodeketa |||| Transkodeketak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"targetFormat": "Helburuko formatua",
|
||||
"defaultBitRate": "Bit tasa, defektuz",
|
||||
"command": "Komandoa"
|
||||
}
|
||||
},
|
||||
"playlist": {
|
||||
"name": "Zerrenda |||| Zerrendak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"duration": "Iraupena",
|
||||
"ownerName": "Jabea",
|
||||
"public": "Publikoa",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"createdAt": "Sortze-data:",
|
||||
"songCount": "abesti",
|
||||
"comment": "Iruzkina",
|
||||
"sync": "Automatikoki inportatuak",
|
||||
"path": "Inportatze-data:"
|
||||
},
|
||||
"actions": {
|
||||
"selectPlaylist": "Hautatu zerrenda:",
|
||||
"addNewPlaylist": "Sortu \"%{name}\"",
|
||||
"export": "Esportatu",
|
||||
"makePublic": "Egin publikoa",
|
||||
"makePrivate": "Egin pribatua"
|
||||
},
|
||||
"message": {
|
||||
"duplicate_song": "Hautatutako abesti batzuk lehendik ere daude zerrendan",
|
||||
"song_exist": "Bikoiztutakoak gehitzen ari dira erreprodukzio-zerrendara. Ziur gehitu nahi dituzula?"
|
||||
}
|
||||
},
|
||||
"radio": {
|
||||
"name": "Irratia |||| Irratiak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"streamUrl": "Jarioaren URLa",
|
||||
"homePageUrl": "Web orriaren URLa",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"createdAt": "Sortze-data:"
|
||||
},
|
||||
"actions": {
|
||||
"playNow": "Erreproduzitu orain"
|
||||
}
|
||||
},
|
||||
"share": {
|
||||
"name": "Partekatu",
|
||||
"fields": {
|
||||
"username": "Partekatzailea:",
|
||||
"url": "URLa",
|
||||
"description": "Deskribapena",
|
||||
"downloadable": "Deskargatzea ahalbidetu?",
|
||||
"contents": "Edukia",
|
||||
"expiresAt": "Iraungitze-data:",
|
||||
"lastVisitedAt": "Azkenekoz bisitatu zen:",
|
||||
"visitCount": "Bisita kopurua",
|
||||
"format": "Formatua",
|
||||
"maxBitRate": "Gehienezko bit tasa",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"createdAt": "Sortze-data:"
|
||||
},
|
||||
"notifications": {},
|
||||
"actions": {}
|
||||
}
|
||||
"languageName": "Euskara",
|
||||
"resources": {
|
||||
"song": {
|
||||
"name": "Abestia |||| Abestiak",
|
||||
"fields": {
|
||||
"albumArtist": "Albumaren artista",
|
||||
"duration": "Iraupena",
|
||||
"trackNumber": "#",
|
||||
"playCount": "Erreprodukzioak",
|
||||
"title": "Titulua",
|
||||
"artist": "Artista",
|
||||
"album": "Albuma",
|
||||
"path": "Fitxategiaren bidea",
|
||||
"genre": "Generoa",
|
||||
"compilation": "Konpilazioa",
|
||||
"year": "Urtea",
|
||||
"size": "Fitxategiaren tamaina",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"bitRate": "Bit tasa",
|
||||
"discSubtitle": "Diskoaren azpititulua",
|
||||
"starred": "Gogokoa",
|
||||
"comment": "Iruzkina",
|
||||
"rating": "Balorazioa",
|
||||
"quality": "Kalitatea",
|
||||
"bpm": "BPM",
|
||||
"playDate": "Azkenekoz erreproduzitua:",
|
||||
"channels": "Kanalak",
|
||||
"createdAt": "Gehitu zen data:",
|
||||
"grouping": "",
|
||||
"mood": "",
|
||||
"participants": "",
|
||||
"tags": "",
|
||||
"mappedTags": "",
|
||||
"rawTags": ""
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "Erreproduzitu ondoren",
|
||||
"playNow": "Erreproduzitu orain",
|
||||
"addToPlaylist": "Gehitu erreprodukzio-zerrendara",
|
||||
"shuffleAll": "Erreprodukzio aleatorioa",
|
||||
"download": "Deskargatu",
|
||||
"playNext": "Hurrengoa",
|
||||
"info": "Lortu informazioa"
|
||||
}
|
||||
},
|
||||
"ra": {
|
||||
"auth": {
|
||||
"welcome1": "Eskerrik asko Navidrome instalatzeagatik!",
|
||||
"welcome2": "Lehenik eta behin, sortu administratzaile kontua",
|
||||
"confirmPassword": "Baieztatu pasahitza",
|
||||
"buttonCreateAdmin": "Sortu administratzailea",
|
||||
"auth_check_error": "Hasi saioa aurrera egiteko",
|
||||
"user_menu": "Profila",
|
||||
"username": "Erabiltzailea",
|
||||
"password": "Pasahitza",
|
||||
"sign_in": "Sartu",
|
||||
"sign_in_error": "Autentifikazioak huts egin du, saiatu berriro",
|
||||
"logout": "Amaitu saioa",
|
||||
"insightsCollectionNote": "Navidromek erabilera-datu anonimoak biltzen ditu\nproiektua hobetzeko asmoz. Klikatu [hemen] gehiago ikasteko\neta, hala nahi izanez gero, parte hartzen uzteko"
|
||||
},
|
||||
"validation": {
|
||||
"invalidChars": "Erabili hizkiak eta zenbakiak bakarrik",
|
||||
"passwordDoesNotMatch": "Pasahitzak ez datoz bat",
|
||||
"required": "Beharrezkoa",
|
||||
"minLength": "Gutxienez %{min} karaktere izan behar ditu",
|
||||
"maxLength": "Gehienez %{max} karaktere izan ditzake",
|
||||
"minValue": "Gutxienez %{min} izan behar da",
|
||||
"maxValue": "Gehienez %{max} izan daiteke",
|
||||
"number": "Zenbakia izan behar da",
|
||||
"email": "Baliozko ePosta helbidea izan behar da",
|
||||
"oneOf": "Hauetako bat izan behar da: %{options}",
|
||||
"regex": "Formatu zehatzarekin bat etorri behar da (regexp): %{pattern}",
|
||||
"unique": "Bakarra izan behar da",
|
||||
"url": "Baliozko URLa izan behar da"
|
||||
},
|
||||
"action": {
|
||||
"add_filter": "Gehitu iragazkia",
|
||||
"add": "Gehitu",
|
||||
"back": "Itzuli",
|
||||
"bulk_actions": "elementu 1 hautatuta |||| %{smart_count} elementu hautatuta",
|
||||
"cancel": "Utzi",
|
||||
"clear_input_value": "Garbitu balioa",
|
||||
"clone": "Bikoiztu",
|
||||
"confirm": "Baieztatu",
|
||||
"create": "Sortu",
|
||||
"delete": "Ezabatu",
|
||||
"edit": "Editatu",
|
||||
"export": "Esportatu",
|
||||
"list": "Zerrenda",
|
||||
"refresh": "Freskatu",
|
||||
"remove_filter": "Ezabatu iragazkia",
|
||||
"remove": "Ezabatu",
|
||||
"save": "Gorde",
|
||||
"search": "Bilatu",
|
||||
"show": "Erakutsi",
|
||||
"sort": "Ordenatu",
|
||||
"undo": "Desegin",
|
||||
"expand": "Hedatu",
|
||||
"close": "Itxi",
|
||||
"open_menu": "Ireki menua",
|
||||
"close_menu": "Itxi menua",
|
||||
"unselect": "Utzi hautatzeari",
|
||||
"skip": "Utzi alde batera",
|
||||
"bulk_actions_mobile": "1 |||| %{smart_count}",
|
||||
"share": "Partekatu",
|
||||
"download": "Deskargatu"
|
||||
},
|
||||
"boolean": {
|
||||
"true": "Bai",
|
||||
"false": "Ez"
|
||||
},
|
||||
"page": {
|
||||
"create": "Sortu %{name}",
|
||||
"dashboard": "Mahaigaina",
|
||||
"edit": "%{name} #%{id}",
|
||||
"error": "Zerbaitek huts egin du",
|
||||
"list": "%{name}",
|
||||
"loading": "Kargatzen",
|
||||
"not_found": "Ez da aurkitu",
|
||||
"show": "%{name} #%{id}",
|
||||
"empty": "Oraindik ez dago %{name}(r)ik.",
|
||||
"invite": "Sortu nahi al duzu?"
|
||||
},
|
||||
"input": {
|
||||
"file": {
|
||||
"upload_several": "Jaregin edo hautatu igo nahi dituzun fitxategiak.",
|
||||
"upload_single": "AJaregin edo hautatu igo nahi duzun fitxategia."
|
||||
},
|
||||
"image": {
|
||||
"upload_several": "Jaregin edo hautatu igo nahi dituzun irudiak.",
|
||||
"upload_single": "Jaregin edo hautatu igo nahi duzun irudia."
|
||||
},
|
||||
"references": {
|
||||
"all_missing": "Ezin dira erreferentziazko datuak aurkitu.",
|
||||
"many_missing": "Erreferentzietako bat gutxieenez ez dago eskuragai.",
|
||||
"single_missing": "Ez dirudi erreferentzia eskuragai dagoenik."
|
||||
},
|
||||
"password": {
|
||||
"toggle_visible": "Ezkutatu pasahitza",
|
||||
"toggle_hidden": "Erakutsi pasahitza"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"about": "Honi buruz",
|
||||
"are_you_sure": "Ziur zaude?",
|
||||
"bulk_delete_content": "Ziur %{name} ezabatu nahi duzula? |||| Ziur %{smart_count} hauek ezabatu nahi dituzula?",
|
||||
"bulk_delete_title": "Ezabatu %{name} |||| Ezabatu %{smart_count} %{name}",
|
||||
"delete_content": "Ziur elementu hau ezabatu nahi duzula?",
|
||||
"delete_title": "Ezabatu %{name} #%{id}",
|
||||
"details": "Xehetasunak",
|
||||
"error": "Bezeroan errorea gertatu da eta eskaera ezin izan da gauzatu",
|
||||
"invalid_form": "Formularioa ez da baliozkoa. Egiaztatu errorerik ez dagoela",
|
||||
"loading": "Orria kargatzen ari da, itxaron",
|
||||
"no": "Ez",
|
||||
"not_found": "URLa ez da zuzena edo jarraitutako esteka akastuna da.",
|
||||
"yes": "Bai",
|
||||
"unsaved_changes": "Ez dira aldaketa batzuk gorde. Ziur muzin egin nahi diezula?"
|
||||
},
|
||||
"navigation": {
|
||||
"no_results": "Ez da emaitzarik aurkitu",
|
||||
"no_more_results": "%{page} orrialde-zenbakia mugetatik kanpo dago. Saiatu aurreko orrialdearekin.",
|
||||
"page_out_of_boundaries": "%{page} orrialde-zenbakia mugetatik kanpo dago",
|
||||
"page_out_from_end": "Ezin zara azken orrialdea baino haratago joan",
|
||||
"page_out_from_begin": "Ezin zara lehenengo orrialdea baino aurrerago joan",
|
||||
"page_range_info": "%{offsetBegin}-%{offsetEnd}, %{total} guztira",
|
||||
"page_rows_per_page": "Errenkadak orrialdeko:",
|
||||
"next": "Hurrengoa",
|
||||
"prev": "Aurrekoa",
|
||||
"skip_nav": "Joan edukira"
|
||||
},
|
||||
"notification": {
|
||||
"updated": "Elementu bat eguneratu da |||| %{smart_count} elementu eguneratu dira",
|
||||
"created": "Elementua sortu da",
|
||||
"deleted": "Elementu bat ezabatu da |||| %{smart_count} elementu ezabatu dira.",
|
||||
"bad_item": "Elementu okerra",
|
||||
"item_doesnt_exist": "Elementua ez dago",
|
||||
"http_error": "Errorea zerbitzariarekin komunikatzerakoan",
|
||||
"data_provider_error": "Errorea datuen hornitzailean. Berrikusi kontsola xehetasun gehiagorako.",
|
||||
"i18n_error": "Ezin izan dira zehaztutako hizkuntzaren itzulpenak kargatu",
|
||||
"canceled": "Ekintza bertan behera utzi da",
|
||||
"logged_out": "Saioa amaitu da, konektatu berriro.",
|
||||
"new_version": "Bertsio berria eskuragai! Freskatu leihoa."
|
||||
},
|
||||
"toggleFieldsMenu": {
|
||||
"columnsToDisplay": "Erakusteko zutabeak",
|
||||
"layout": "Antolaketa",
|
||||
"grid": "Sareta",
|
||||
"table": "Taula"
|
||||
}
|
||||
"album": {
|
||||
"name": "Albuma |||| Albumak",
|
||||
"fields": {
|
||||
"albumArtist": "Albumaren artista",
|
||||
"artist": "Artista",
|
||||
"duration": "Iraupena",
|
||||
"songCount": "abesti",
|
||||
"playCount": "Erreprodukzioak",
|
||||
"name": "Izena",
|
||||
"genre": "Generoa",
|
||||
"compilation": "Konpilazioa",
|
||||
"year": "Urtea",
|
||||
"updatedAt": "Aktualizatze-data:",
|
||||
"comment": "Iruzkina",
|
||||
"rating": "Balorazioa",
|
||||
"createdAt": "Gehitu zen data:",
|
||||
"size": "Fitxategiaren tamaina",
|
||||
"originalDate": "Jatorrizkoa",
|
||||
"releaseDate": "Argitaratze-data:",
|
||||
"releases": "Argitaratzea |||| Argitaratzeak",
|
||||
"released": "Argitaratua",
|
||||
"recordLabel": "",
|
||||
"catalogNum": "",
|
||||
"releaseType": "",
|
||||
"grouping": "",
|
||||
"media": "",
|
||||
"mood": ""
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "Erreproduzitu",
|
||||
"playNext": "Erreproduzitu segidan",
|
||||
"addToQueue": "Erreproduzitu amaieran",
|
||||
"shuffle": "Aletorioa",
|
||||
"addToPlaylist": "Gehitu zerrendara",
|
||||
"download": "Deskargatu",
|
||||
"info": "Lortu informazioa",
|
||||
"share": "Partekatu"
|
||||
},
|
||||
"lists": {
|
||||
"all": "Guztiak",
|
||||
"random": "Aleatorioa",
|
||||
"recentlyAdded": "Berriki gehitutakoak",
|
||||
"recentlyPlayed": "Berriki entzundakoak",
|
||||
"mostPlayed": "Gehien entzundakoak",
|
||||
"starred": "Gogokoak",
|
||||
"topRated": "Hobekien baloratutakoak"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"note": "OHARRA",
|
||||
"transcodingDisabled": "Segurtasun arrazoiak direla-eta, transkodeketaren ezarpenak web-interfazearen bidez aldatzea ezgaituta dago. Transkodeketa-aukerak aldatu (editatu edo gehitu) nahi badituzu, berrabiarazi zerbitzaria konfigurazio-aukeraren %{config}-arekin.",
|
||||
"transcodingEnabled": "Navidrome %{config}-ekin martxan dago eta, beraz, web-interfazeko transkodeketa-ataletik sistema-komandoak exekuta daitezke. Segurtasun arrazoiak tarteko, ezgaitzea gomendatzen dugu, eta transkodeketa-aukerak konfiguratzen ari zarenean bakarrik gaitzea.",
|
||||
"songsAddedToPlaylist": "Abesti bat zerrendara gehitu da |||| %{smart_count} abesti zerrendara gehitu dira",
|
||||
"noPlaylistsAvailable": "Ez dago zerrendarik erabilgarri",
|
||||
"delete_user_title": "Ezabatu '%{name}' erabiltzailea",
|
||||
"delete_user_content": "Ziur zaide erabiltzaile hau eta bere datu guztiak (zerrendak eta hobespenak barne) ezabatu nahi dituzula?",
|
||||
"notifications_blocked": "Nabigatzaileak jakinarazpenak blokeatzen ditu",
|
||||
"notifications_not_available": "Nabigatzaile hau ez da jakinarazpenekin bateragarria edo Navidrome ez da HTTPS erabiltzen ari",
|
||||
"lastfmLinkSuccess": "Last.fm konektatuta dago eta erabiltzailearen ohiturak hirugarrenen zerbitzuekin partekatzea gaituta dago",
|
||||
"lastfmLinkFailure": "Ezin izan da Last.fm-rekin konektatu",
|
||||
"lastfmUnlinkSuccess": "Last.fm deskonektatu da eta erabiltzailearen ohiturak hirugarrenen zerbitzuekin partekatzea ezgaitu da",
|
||||
"lastfmUnlinkFailure": "Ezin izan da Last.fm deskonektatu",
|
||||
"listenBrainzLinkSuccess": "Ondo konektatu da ListenBrainz-ekin eta %{user} erabiltzailearen ohiturak hirugarrenen zerbitzuekin partekatzea aktibatu da",
|
||||
"listenBrainzLinkFailure": "Ezin izan da ListenBrainz-ekin konektatu: %{error}",
|
||||
"listenBrainzUnlinkSuccess": "ListenBrainz deskonektatu da eta erabiltzailearen ohiturak hirugarrenen zerbitzuekin partekatzea desaktibatu da",
|
||||
"listenBrainzUnlinkFailure": "Ezin izan da ListenBrainz deskonektatu",
|
||||
"openIn": {
|
||||
"lastfm": "Ireki Last.fm-n",
|
||||
"musicbrainz": "Ireki MusicBrainz-en"
|
||||
},
|
||||
"lastfmLink": "Irakurri gehiago…",
|
||||
"downloadOriginalFormat": "Deskargatu jatorrizko formatua",
|
||||
"shareOriginalFormat": "Partekatu jatorrizko formatua",
|
||||
"shareDialogTitle": "Partekatu '%{name}' %{resource}",
|
||||
"shareBatchDialogTitle": "Partekatu %{resource} bat |||| Partekatu %{smart_count} %{resource}",
|
||||
"shareSuccess": "URLa arbelera kopiatu da: %{url}",
|
||||
"shareFailure": "Errorea %{url} URLa arbelera kopiatzean",
|
||||
"downloadDialogTitle": "Deskargatu '%{name}' %{resource}, (%{size})",
|
||||
"shareCopyToClipboard": "Kopiatu arbelera: Ktrl + C, Sartu tekla"
|
||||
"artist": {
|
||||
"name": "Artista |||| Artistak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"albumCount": "Album kopurua",
|
||||
"songCount": "Abesti kopurua",
|
||||
"playCount": "Erreprodukzio kopurua",
|
||||
"rating": "Balorazioa",
|
||||
"genre": "Generoa",
|
||||
"size": "Tamaina",
|
||||
"role": ""
|
||||
},
|
||||
"roles": {
|
||||
"albumartist": "",
|
||||
"artist": "",
|
||||
"composer": "",
|
||||
"conductor": "",
|
||||
"lyricist": "",
|
||||
"arranger": "",
|
||||
"producer": "",
|
||||
"director": "",
|
||||
"engineer": "",
|
||||
"mixer": "",
|
||||
"remixer": "",
|
||||
"djmixer": "",
|
||||
"performer": ""
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"library": "Liburutegia",
|
||||
"settings": "Ezarpenak",
|
||||
"version": "Bertsioa",
|
||||
"theme": "Itxura",
|
||||
"personal": {
|
||||
"name": "Pertsonala",
|
||||
"options": {
|
||||
"theme": "Itxura",
|
||||
"language": "Hizkuntza",
|
||||
"defaultView": "Bista, defektuz",
|
||||
"desktop_notifications": "Mahaigaineko jakinarazpenak",
|
||||
"lastfmNotConfigured": "Ez da Last.fm-ren API gakoa konfiguratu",
|
||||
"lastfmScrobbling": "Bidali Last.fm-ra erabiltzailearen ohiturak",
|
||||
"listenBrainzScrobbling": "Bidali ListenBrainz-era erabiltzailearen ohiturak",
|
||||
"replaygain": "ReplayGain modua",
|
||||
"preAmp": "ReplayGain PreAmp (dB)",
|
||||
"gain": {
|
||||
"none": "Bat ere ez",
|
||||
"album": "Albuma",
|
||||
"track": "Pista"
|
||||
}
|
||||
}
|
||||
},
|
||||
"albumList": "Albumak",
|
||||
"playlists": "Zerrendak",
|
||||
"sharedPlaylists": "Partekatutako erreprodukzio-zerrendak",
|
||||
"about": "Honi buruz"
|
||||
"user": {
|
||||
"name": "Erabiltzailea |||| Erabiltzaileak",
|
||||
"fields": {
|
||||
"userName": "Erabiltzailearen izena",
|
||||
"isAdmin": "Administratzailea da",
|
||||
"lastLoginAt": "Azken saio hasiera:",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"name": "Izena",
|
||||
"password": "Pasahitza",
|
||||
"createdAt": "Sortze-data:",
|
||||
"changePassword": "Pasahitza aldatu?",
|
||||
"currentPassword": "Uneko pasahitza",
|
||||
"newPassword": "Pasahitz berria",
|
||||
"token": "Tokena",
|
||||
"lastAccessAt": "Azken sarbidea"
|
||||
},
|
||||
"helperTexts": {
|
||||
"name": "Aldaketak saioa hasten duzun hurrengoan islatuko dira"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Erabiltzailea sortu da",
|
||||
"updated": "Erabiltzailea eguneratu da",
|
||||
"deleted": "Erabiltzailea ezabatu da"
|
||||
},
|
||||
"message": {
|
||||
"listenBrainzToken": "Idatzi zure ListenBrainz erabiltzailearen tokena",
|
||||
"clickHereForToken": "Egin klik hemen tokena lortzeko"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"playListsText": "Erreprodukzio-zerrenda",
|
||||
"openText": "Ireki",
|
||||
"closeText": "Itxi",
|
||||
"notContentText": "Ez dago musikarik",
|
||||
"clickToPlayText": "Egin klik erreproduzitzeko",
|
||||
"clickToPauseText": "Egin klik eteteko",
|
||||
"nextTrackText": "Hurrengo pista",
|
||||
"previousTrackText": "Aurreko pista",
|
||||
"reloadText": "Freskatu",
|
||||
"volumeText": "Bolumena",
|
||||
"toggleLyricText": "Erakutsi letrak",
|
||||
"toggleMiniModeText": "Ikonotu",
|
||||
"destroyText": "Suntsitu",
|
||||
"downloadText": "Deskargatu",
|
||||
"removeAudioListsText": "Ezabatu audio-zerrendak",
|
||||
"clickToDeleteText": "Egin klik %{name} ezabatzeko",
|
||||
"emptyLyricText": "Ez dago letrarik",
|
||||
"playModeText": {
|
||||
"order": "Ordenean",
|
||||
"orderLoop": "Errepikatu",
|
||||
"singleLoop": "Errepikatu bakarra",
|
||||
"shufflePlay": "Aleatorioa"
|
||||
}
|
||||
"name": "Erreproduktorea |||| Erreproduktoreak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"transcodingId": "Transkodifikazioa",
|
||||
"maxBitRate": "Gehienezko bit tasa",
|
||||
"client": "Bezeroa",
|
||||
"userName": "Erabiltzailea",
|
||||
"lastSeen": "Azken konexioa",
|
||||
"reportRealPath": "Erakutsi bide absolutua",
|
||||
"scrobbleEnabled": "Bidali erabiltzailearen ohiturak hirugarrenen zerbitzuetara"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"links": {
|
||||
"homepage": "Hasierako orria",
|
||||
"source": "Iturburu kodea",
|
||||
"featureRequests": "Eskatu ezaugarria",
|
||||
"lastInsightsCollection": "Bildutako azken datuak",
|
||||
"insights": {
|
||||
"disabled": "Ezgaituta",
|
||||
"waiting": "Zain"
|
||||
"transcoding": {
|
||||
"name": "Transkodeketa |||| Transkodeketak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"targetFormat": "Helburuko formatua",
|
||||
"defaultBitRate": "Bit tasa, defektuz",
|
||||
"command": "Komandoa"
|
||||
}
|
||||
},
|
||||
"playlist": {
|
||||
"name": "Zerrenda |||| Zerrendak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"duration": "Iraupena",
|
||||
"ownerName": "Jabea",
|
||||
"public": "Publikoa",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"createdAt": "Sortze-data:",
|
||||
"songCount": "abesti",
|
||||
"comment": "Iruzkina",
|
||||
"sync": "Automatikoki inportatuak",
|
||||
"path": "Inportatze-data:"
|
||||
},
|
||||
"actions": {
|
||||
"selectPlaylist": "Hautatu zerrenda:",
|
||||
"addNewPlaylist": "Sortu \"%{name}\"",
|
||||
"export": "Esportatu",
|
||||
"makePublic": "Egin publikoa",
|
||||
"makePrivate": "Egin pribatua"
|
||||
},
|
||||
"message": {
|
||||
"duplicate_song": "Hautatutako abesti batzuk lehendik ere daude zerrendan",
|
||||
"song_exist": "Bikoiztutakoak gehitzen ari dira erreprodukzio-zerrendara. Ziur gehitu nahi dituzula?"
|
||||
}
|
||||
},
|
||||
"radio": {
|
||||
"name": "Irratia |||| Irratiak",
|
||||
"fields": {
|
||||
"name": "Izena",
|
||||
"streamUrl": "Jarioaren URLa",
|
||||
"homePageUrl": "Web orriaren URLa",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"createdAt": "Sortze-data:"
|
||||
},
|
||||
"actions": {
|
||||
"playNow": "Erreproduzitu orain"
|
||||
}
|
||||
},
|
||||
"share": {
|
||||
"name": "Partekatu",
|
||||
"fields": {
|
||||
"username": "Partekatzailea:",
|
||||
"url": "URLa",
|
||||
"description": "Deskribapena",
|
||||
"contents": "Edukia",
|
||||
"expiresAt": "Iraungitze-data:",
|
||||
"lastVisitedAt": "Azkenekoz bisitatu zen:",
|
||||
"visitCount": "Bisita kopurua",
|
||||
"format": "Formatua",
|
||||
"maxBitRate": "Gehienezko bit tasa",
|
||||
"updatedAt": "Eguneratze-data:",
|
||||
"createdAt": "Sortze-data:",
|
||||
"downloadable": "Deskargatzea ahalbidetu?"
|
||||
}
|
||||
},
|
||||
"missing": {
|
||||
"name": "",
|
||||
"fields": {
|
||||
"path": "",
|
||||
"size": "",
|
||||
"updatedAt": ""
|
||||
},
|
||||
"actions": {
|
||||
"remove": ""
|
||||
},
|
||||
"notifications": {
|
||||
"removed": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"title": "Ekintzak",
|
||||
"totalScanned": "Arakatutako karpeta guztiak",
|
||||
"quickScan": "Arakatze azkarra",
|
||||
"fullScan": "Arakatze sakona",
|
||||
"serverUptime": "Zerbitzariak piztuta daraman denbora",
|
||||
"serverDown": "LINEAZ KANPO"
|
||||
"ra": {
|
||||
"auth": {
|
||||
"welcome1": "Eskerrik asko Navidrome instalatzeagatik!",
|
||||
"welcome2": "Lehenik eta behin, sortu administratzaile kontua",
|
||||
"confirmPassword": "Baieztatu pasahitza",
|
||||
"buttonCreateAdmin": "Sortu administratzailea",
|
||||
"auth_check_error": "Hasi saioa aurrera egiteko",
|
||||
"user_menu": "Profila",
|
||||
"username": "Erabiltzailea",
|
||||
"password": "Pasahitza",
|
||||
"sign_in": "Sartu",
|
||||
"sign_in_error": "Autentifikazioak huts egin du, saiatu berriro",
|
||||
"logout": "Amaitu saioa",
|
||||
"insightsCollectionNote": ""
|
||||
},
|
||||
"help": {
|
||||
"title": "Navidromeren laster-teklak",
|
||||
"hotkeys": {
|
||||
"show_help": "Erakutsi laguntza",
|
||||
"toggle_menu": "Alboko barra bai / ez",
|
||||
"toggle_play": "Erreproduzitu / Eten",
|
||||
"prev_song": "Aurreko abestia",
|
||||
"next_song": "Hurrengo abestia",
|
||||
"vol_up": "Igo bolumena",
|
||||
"vol_down": "Jaitsi bolumena",
|
||||
"toggle_love": "Abestia gogoko bai / ez",
|
||||
"current_song": "Uneko abestia"
|
||||
}
|
||||
"validation": {
|
||||
"invalidChars": "Erabili hizkiak eta zenbakiak bakarrik",
|
||||
"passwordDoesNotMatch": "Pasahitzak ez datoz bat",
|
||||
"required": "Beharrezkoa",
|
||||
"minLength": "Gutxienez %{min} karaktere izan behar ditu",
|
||||
"maxLength": "Gehienez %{max} karaktere izan ditzake",
|
||||
"minValue": "Gutxienez %{min} izan behar da",
|
||||
"maxValue": "Gehienez %{max} izan daiteke",
|
||||
"number": "Zenbakia izan behar da",
|
||||
"email": "Baliozko ePosta helbidea izan behar da",
|
||||
"oneOf": "Hauetako bat izan behar da: %{options}",
|
||||
"regex": "Formatu zehatzarekin bat etorri behar da (regexp): %{pattern}",
|
||||
"unique": "Bakarra izan behar da",
|
||||
"url": "Baliozko URLa izan behar da"
|
||||
},
|
||||
"action": {
|
||||
"add_filter": "Gehitu iragazkia",
|
||||
"add": "Gehitu",
|
||||
"back": "Itzuli",
|
||||
"bulk_actions": "elementu 1 hautatuta |||| %{smart_count} elementu hautatuta",
|
||||
"cancel": "Utzi",
|
||||
"clear_input_value": "Garbitu balioa",
|
||||
"clone": "Bikoiztu",
|
||||
"confirm": "Baieztatu",
|
||||
"create": "Sortu",
|
||||
"delete": "Ezabatu",
|
||||
"edit": "Editatu",
|
||||
"export": "Esportatu",
|
||||
"list": "Zerrenda",
|
||||
"refresh": "Freskatu",
|
||||
"remove_filter": "Ezabatu iragazkia",
|
||||
"remove": "Ezabatu",
|
||||
"save": "Gorde",
|
||||
"search": "Bilatu",
|
||||
"show": "Erakutsi",
|
||||
"sort": "Ordenatu",
|
||||
"undo": "Desegin",
|
||||
"expand": "Hedatu",
|
||||
"close": "Itxi",
|
||||
"open_menu": "Ireki menua",
|
||||
"close_menu": "Itxi menua",
|
||||
"unselect": "Utzi hautatzeari",
|
||||
"skip": "Utzi alde batera",
|
||||
"bulk_actions_mobile": "1 |||| %{smart_count}",
|
||||
"share": "Partekatu",
|
||||
"download": "Deskargatu"
|
||||
},
|
||||
"boolean": {
|
||||
"true": "Bai",
|
||||
"false": "Ez"
|
||||
},
|
||||
"page": {
|
||||
"create": "Sortu %{name}",
|
||||
"dashboard": "Mahaigaina",
|
||||
"edit": "%{name} #%{id}",
|
||||
"error": "Zerbaitek huts egin du",
|
||||
"list": "%{name}",
|
||||
"loading": "Kargatzen",
|
||||
"not_found": "Ez da aurkitu",
|
||||
"show": "%{name} #%{id}",
|
||||
"empty": "Oraindik ez dago %{name}(r)ik.",
|
||||
"invite": "Sortu nahi al duzu?"
|
||||
},
|
||||
"input": {
|
||||
"file": {
|
||||
"upload_several": "Jaregin edo hautatu igo nahi dituzun fitxategiak.",
|
||||
"upload_single": "AJaregin edo hautatu igo nahi duzun fitxategia."
|
||||
},
|
||||
"image": {
|
||||
"upload_several": "Jaregin edo hautatu igo nahi dituzun irudiak.",
|
||||
"upload_single": "Jaregin edo hautatu igo nahi duzun irudia."
|
||||
},
|
||||
"references": {
|
||||
"all_missing": "Ezin dira erreferentziazko datuak aurkitu.",
|
||||
"many_missing": "Erreferentzietako bat gutxieenez ez dago eskuragai.",
|
||||
"single_missing": "Ez dirudi erreferentzia eskuragai dagoenik."
|
||||
},
|
||||
"password": {
|
||||
"toggle_visible": "Ezkutatu pasahitza",
|
||||
"toggle_hidden": "Erakutsi pasahitza"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"about": "Honi buruz",
|
||||
"are_you_sure": "Ziur zaude?",
|
||||
"bulk_delete_content": "Ziur %{name} ezabatu nahi duzula? |||| Ziur %{smart_count} hauek ezabatu nahi dituzula?",
|
||||
"bulk_delete_title": "Ezabatu %{name} |||| Ezabatu %{smart_count} %{name}",
|
||||
"delete_content": "Ziur elementu hau ezabatu nahi duzula?",
|
||||
"delete_title": "Ezabatu %{name} #%{id}",
|
||||
"details": "Xehetasunak",
|
||||
"error": "Bezeroan errorea gertatu da eta eskaera ezin izan da gauzatu",
|
||||
"invalid_form": "Formularioa ez da baliozkoa. Egiaztatu errorerik ez dagoela",
|
||||
"loading": "Orria kargatzen ari da, itxaron",
|
||||
"no": "Ez",
|
||||
"not_found": "URLa ez da zuzena edo jarraitutako esteka akastuna da.",
|
||||
"yes": "Bai",
|
||||
"unsaved_changes": "Ez dira aldaketa batzuk gorde. Ziur muzin egin nahi diezula?"
|
||||
},
|
||||
"navigation": {
|
||||
"no_results": "Ez da emaitzarik aurkitu",
|
||||
"no_more_results": "%{page} orrialde-zenbakia mugetatik kanpo dago. Saiatu aurreko orrialdearekin.",
|
||||
"page_out_of_boundaries": "%{page} orrialde-zenbakia mugetatik kanpo dago",
|
||||
"page_out_from_end": "Ezin zara azken orrialdea baino haratago joan",
|
||||
"page_out_from_begin": "Ezin zara lehenengo orrialdea baino aurrerago joan",
|
||||
"page_range_info": "%{offsetBegin}-%{offsetEnd}, %{total} guztira",
|
||||
"page_rows_per_page": "Errenkadak orrialdeko:",
|
||||
"next": "Hurrengoa",
|
||||
"prev": "Aurrekoa",
|
||||
"skip_nav": "Joan edukira"
|
||||
},
|
||||
"notification": {
|
||||
"updated": "Elementu bat eguneratu da |||| %{smart_count} elementu eguneratu dira",
|
||||
"created": "Elementua sortu da",
|
||||
"deleted": "Elementu bat ezabatu da |||| %{smart_count} elementu ezabatu dira.",
|
||||
"bad_item": "Elementu okerra",
|
||||
"item_doesnt_exist": "Elementua ez dago",
|
||||
"http_error": "Errorea zerbitzariarekin komunikatzerakoan",
|
||||
"data_provider_error": "Errorea datuen hornitzailean. Berrikusi kontsola xehetasun gehiagorako.",
|
||||
"i18n_error": "Ezin izan dira zehaztutako hizkuntzaren itzulpenak kargatu",
|
||||
"canceled": "Ekintza bertan behera utzi da",
|
||||
"logged_out": "Saioa amaitu da, konektatu berriro.",
|
||||
"new_version": "Bertsio berria eskuragai! Freskatu leihoa."
|
||||
},
|
||||
"toggleFieldsMenu": {
|
||||
"columnsToDisplay": "Erakusteko zutabeak",
|
||||
"layout": "Antolaketa",
|
||||
"grid": "Sareta",
|
||||
"table": "Taula"
|
||||
}
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"note": "OHARRA",
|
||||
"transcodingDisabled": "Segurtasun arrazoiak direla-eta, transkodeketaren ezarpenak web-interfazearen bidez aldatzea ezgaituta dago. Transkodeketa-aukerak aldatu (editatu edo gehitu) nahi badituzu, berrabiarazi zerbitzaria konfigurazio-aukeraren %{config}-arekin.",
|
||||
"transcodingEnabled": "Navidrome %{config}-ekin martxan dago eta, beraz, web-interfazeko transkodeketa-ataletik sistema-komandoak exekuta daitezke. Segurtasun arrazoiak tarteko, ezgaitzea gomendatzen dugu, eta transkodeketa-aukerak konfiguratzen ari zarenean bakarrik gaitzea.",
|
||||
"songsAddedToPlaylist": "Abesti bat zerrendara gehitu da |||| %{smart_count} abesti zerrendara gehitu dira",
|
||||
"noPlaylistsAvailable": "Ez dago zerrendarik erabilgarri",
|
||||
"delete_user_title": "Ezabatu '%{name}' erabiltzailea",
|
||||
"delete_user_content": "Ziur zaide erabiltzaile hau eta bere datu guztiak (zerrendak eta hobespenak barne) ezabatu nahi dituzula?",
|
||||
"notifications_blocked": "Nabigatzaileak jakinarazpenak blokeatzen ditu",
|
||||
"notifications_not_available": "Nabigatzaile hau ez da jakinarazpenekin bateragarria edo Navidrome ez da HTTPS erabiltzen ari",
|
||||
"lastfmLinkSuccess": "Last.fm konektatuta dago eta erabiltzailearen ohiturak hirugarrenen zerbitzuekin partekatzea gaituta dago",
|
||||
"lastfmLinkFailure": "Ezin izan da Last.fm-rekin konektatu",
|
||||
"lastfmUnlinkSuccess": "Last.fm deskonektatu da eta erabiltzailearen ohiturak hirugarrenen zerbitzuekin partekatzea ezgaitu da",
|
||||
"lastfmUnlinkFailure": "Ezin izan da Last.fm deskonektatu",
|
||||
"openIn": {
|
||||
"lastfm": "Ikusi Last.fm-n",
|
||||
"musicbrainz": "Ikusi MusicBrainz-en"
|
||||
},
|
||||
"lastfmLink": "Irakurri gehiago…",
|
||||
"listenBrainzLinkSuccess": "Ondo konektatu da ListenBrainz-ekin eta %{user} erabiltzailearen ohiturak hirugarrenen zerbitzuekin partekatzea aktibatu da",
|
||||
"listenBrainzLinkFailure": "Ezin izan da ListenBrainz-ekin konektatu: %{error}",
|
||||
"listenBrainzUnlinkSuccess": "ListenBrainz deskonektatu da eta erabiltzailearen ohiturak hirugarrenen zerbitzuekin partekatzea desaktibatu da",
|
||||
"listenBrainzUnlinkFailure": "Ezin izan da ListenBrainz deskonektatu",
|
||||
"downloadOriginalFormat": "Deskargatu jatorrizko formatua",
|
||||
"shareOriginalFormat": "Partekatu jatorrizko formatua",
|
||||
"shareDialogTitle": "Partekatu '%{name}' %{resource}",
|
||||
"shareBatchDialogTitle": "Partekatu %{resource} bat |||| Partekatu %{smart_count} %{resource}",
|
||||
"shareSuccess": "URLa arbelera kopiatu da: %{url}",
|
||||
"shareFailure": "Errorea %{url} URLa arbelera kopiatzean",
|
||||
"downloadDialogTitle": "Deskargatu '%{name}' %{resource}, (%{size})",
|
||||
"shareCopyToClipboard": "Kopiatu arbelera: Ktrl + C, Sartu tekla",
|
||||
"remove_missing_title": "",
|
||||
"remove_missing_content": ""
|
||||
},
|
||||
"menu": {
|
||||
"library": "Liburutegia",
|
||||
"settings": "Ezarpenak",
|
||||
"version": "Bertsioa",
|
||||
"theme": "Itxura",
|
||||
"personal": {
|
||||
"name": "Pertsonala",
|
||||
"options": {
|
||||
"theme": "Itxura",
|
||||
"language": "Hizkuntza",
|
||||
"defaultView": "Bista, defektuz",
|
||||
"desktop_notifications": "Mahaigaineko jakinarazpenak",
|
||||
"lastfmScrobbling": "Bidali Last.fm-ra erabiltzailearen ohiturak",
|
||||
"listenBrainzScrobbling": "Bidali ListenBrainz-era erabiltzailearen ohiturak",
|
||||
"replaygain": "ReplayGain modua",
|
||||
"preAmp": "ReplayGain PreAmp (dB)",
|
||||
"gain": {
|
||||
"none": "Bat ere ez",
|
||||
"album": "Albuma",
|
||||
"track": "Pista"
|
||||
},
|
||||
"lastfmNotConfigured": ""
|
||||
}
|
||||
},
|
||||
"albumList": "Albumak",
|
||||
"about": "Honi buruz",
|
||||
"playlists": "Zerrendak",
|
||||
"sharedPlaylists": "Partekatutako erreprodukzio-zerrendak"
|
||||
},
|
||||
"player": {
|
||||
"playListsText": "Erreprodukzio-zerrenda",
|
||||
"openText": "Ireki",
|
||||
"closeText": "Itxi",
|
||||
"notContentText": "Ez dago musikarik",
|
||||
"clickToPlayText": "Egin klik erreproduzitzeko",
|
||||
"clickToPauseText": "Egin klik eteteko",
|
||||
"nextTrackText": "Hurrengo pista",
|
||||
"previousTrackText": "Aurreko pista",
|
||||
"reloadText": "Freskatu",
|
||||
"volumeText": "Bolumena",
|
||||
"toggleLyricText": "Erakutsi letrak",
|
||||
"toggleMiniModeText": "Ikonotu",
|
||||
"destroyText": "Suntsitu",
|
||||
"downloadText": "Deskargatu",
|
||||
"removeAudioListsText": "Ezabatu audio-zerrendak",
|
||||
"clickToDeleteText": "Egin klik %{name} ezabatzeko",
|
||||
"emptyLyricText": "Ez dago letrarik",
|
||||
"playModeText": {
|
||||
"order": "Ordenean",
|
||||
"orderLoop": "Errepikatu",
|
||||
"singleLoop": "Errepikatu bakarra",
|
||||
"shufflePlay": "Aleatorioa"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"links": {
|
||||
"homepage": "Hasierako orria",
|
||||
"source": "Iturburu kodea",
|
||||
"featureRequests": "Eskatu ezaugarria",
|
||||
"lastInsightsCollection": "",
|
||||
"insights": {
|
||||
"disabled": "",
|
||||
"waiting": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"title": "Ekintzak",
|
||||
"totalScanned": "Arakatutako karpeta guztiak",
|
||||
"quickScan": "Arakatze azkarra",
|
||||
"fullScan": "Arakatze sakona",
|
||||
"serverUptime": "Zerbitzariak piztuta daraman denbora",
|
||||
"serverDown": "LINEAZ KANPO"
|
||||
},
|
||||
"help": {
|
||||
"title": "Navidromeren laster-teklak",
|
||||
"hotkeys": {
|
||||
"show_help": "Erakutsi laguntza",
|
||||
"toggle_menu": "Alboko barra bai / ez",
|
||||
"toggle_play": "Erreproduzitu / Eten",
|
||||
"prev_song": "Aurreko abestia",
|
||||
"next_song": "Hurrengo abestia",
|
||||
"vol_up": "Igo bolumena",
|
||||
"vol_down": "Jaitsi bolumena",
|
||||
"toggle_love": "Abestia gogoko bai / ez",
|
||||
"current_song": "Uneko abestia"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,7 +26,13 @@
|
|||
"bpm": "BPM",
|
||||
"playDate": "Último reproducido",
|
||||
"channels": "Canles",
|
||||
"createdAt": "Engadido"
|
||||
"createdAt": "Engadido",
|
||||
"grouping": "Grupos",
|
||||
"mood": "Estado",
|
||||
"participants": "Participantes adicionais",
|
||||
"tags": "Etiquetas adicionais",
|
||||
"mappedTags": "",
|
||||
"rawTags": "Etiquetas en cru"
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "Ao final da cola",
|
||||
|
@ -58,7 +64,13 @@
|
|||
"originalDate": "Orixinal",
|
||||
"releaseDate": "Publicado",
|
||||
"releases": "Publicación ||| Publicacións",
|
||||
"released": "Publicado"
|
||||
"released": "Publicado",
|
||||
"recordLabel": "Editorial",
|
||||
"catalogNum": "Número de catálogo",
|
||||
"releaseType": "Tipo",
|
||||
"grouping": "Grupos",
|
||||
"media": "Multimedia",
|
||||
"mood": "Estado"
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "Reproducir",
|
||||
|
@ -89,7 +101,23 @@
|
|||
"playCount": "Reproducións",
|
||||
"rating": "Valoración",
|
||||
"genre": "Xénero",
|
||||
"size": "Tamaño"
|
||||
"size": "Tamaño",
|
||||
"role": "Rol"
|
||||
},
|
||||
"roles": {
|
||||
"albumartist": "Artista do álbum |||| Artistas do álbum",
|
||||
"artist": "Artista |||| Artistas",
|
||||
"composer": "Composición |||| Composición",
|
||||
"conductor": "Condutor |||| Condutoras",
|
||||
"lyricist": "Letrista |||| Letristas",
|
||||
"arranger": "Arranxos |||| Arranxos",
|
||||
"producer": "Produtora |||| Produtoras",
|
||||
"director": "Dirección |||| Dirección",
|
||||
"engineer": "Enxeñería |||| Enxeñería",
|
||||
"mixer": "Mistura |||| Mistura",
|
||||
"remixer": "Remezcla |||| Remezcla",
|
||||
"djmixer": "Mezcla DJs |||| Mezcla DJs",
|
||||
"performer": "Intérprete |||| Intérpretes"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
|
@ -198,6 +226,20 @@
|
|||
"createdAt": "Creada o",
|
||||
"downloadable": "Permitir descargas?"
|
||||
}
|
||||
},
|
||||
"missing": {
|
||||
"name": "Falta o ficheiro |||| Faltan os ficheiros",
|
||||
"fields": {
|
||||
"path": "Ruta",
|
||||
"size": "Tamaño",
|
||||
"updatedAt": "Desapareceu o"
|
||||
},
|
||||
"actions": {
|
||||
"remove": "Retirar"
|
||||
},
|
||||
"notifications": {
|
||||
"removed": "Ficheiro(s) faltantes retirados"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ra": {
|
||||
|
@ -212,7 +254,8 @@
|
|||
"password": "Contrasinal",
|
||||
"sign_in": "Accede",
|
||||
"sign_in_error": "Fallou a autenticación, volve intentalo",
|
||||
"logout": "Pechar sesión"
|
||||
"logout": "Pechar sesión",
|
||||
"insightsCollectionNote": "Navidrome recolle datos anónimos de uso para mellorar o proxecto. Peme [aquí] para saber máis e desactivar se queres"
|
||||
},
|
||||
"validation": {
|
||||
"invalidChars": "Utiliza só letras e números",
|
||||
|
@ -374,7 +417,9 @@
|
|||
"shareSuccess": "URL copiado ao portapapeis: %{url}",
|
||||
"shareFailure": "Erro ao copiar o URL %{url} ao portapapeis",
|
||||
"downloadDialogTitle": "Descargar %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "Copiar ao portapapeis: Ctrl+C, Enter"
|
||||
"shareCopyToClipboard": "Copiar ao portapapeis: Ctrl+C, Enter",
|
||||
"remove_missing_title": "Retirar ficheiros que faltan",
|
||||
"remove_missing_content": "Tes certeza de querer retirar da base de datos os ficheiros que faltan? Isto retirará de xeito permanente todas a referencias a eles, incluíndo a conta de reproducións e valoracións."
|
||||
},
|
||||
"menu": {
|
||||
"library": "Biblioteca",
|
||||
|
@ -396,7 +441,8 @@
|
|||
"none": "Desactivada",
|
||||
"album": "Usar ganancia do Álbum",
|
||||
"track": "Usar ganancia da Canción"
|
||||
}
|
||||
},
|
||||
"lastfmNotConfigured": "Clave da API Last.fm non configurada"
|
||||
}
|
||||
},
|
||||
"albumList": "Álbums",
|
||||
|
@ -433,7 +479,12 @@
|
|||
"links": {
|
||||
"homepage": "Inicio",
|
||||
"source": "Código fonte",
|
||||
"featureRequests": "Solicitar funcións"
|
||||
"featureRequests": "Solicitar funcións",
|
||||
"lastInsightsCollection": "Última colección insights",
|
||||
"insights": {
|
||||
"disabled": "Desactivado",
|
||||
"waiting": "Agardando"
|
||||
}
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
|
|
|
@ -1,460 +1,512 @@
|
|||
{
|
||||
"languageName": "Bahasa Indonesia",
|
||||
"resources": {
|
||||
"song": {
|
||||
"name": "Lagu |||| Lagu",
|
||||
"fields": {
|
||||
"albumArtist": "Artis Album",
|
||||
"duration": "Durasi",
|
||||
"trackNumber": "#",
|
||||
"playCount": "Dimainkan",
|
||||
"title": "Judul",
|
||||
"artist": "Artis",
|
||||
"album": "Album",
|
||||
"path": "Jalur file",
|
||||
"genre": "Genre",
|
||||
"compilation": "Kompilasi",
|
||||
"year": "Tahun",
|
||||
"size": "Ukuran file",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"bitRate": "Laju bit",
|
||||
"discSubtitle": "Subtitle Disk",
|
||||
"starred": "Favorit",
|
||||
"comment": "Komentar",
|
||||
"rating": "Peringkat",
|
||||
"quality": "Kualitas",
|
||||
"bpm": "BPM",
|
||||
"playDate": "Terakhir Dimainkan",
|
||||
"channels": "Saluran",
|
||||
"createdAt": "Tgl. Ditambahkan"
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "Tambah ke antrean",
|
||||
"playNow": "Mainkan sekarang",
|
||||
"addToPlaylist": "Tambahkan ke Playlist",
|
||||
"shuffleAll": "Mainkan Acak",
|
||||
"download": "Unduh",
|
||||
"playNext": "Mainkan selanjutnya",
|
||||
"info": "Lihat Info"
|
||||
}
|
||||
},
|
||||
"album": {
|
||||
"name": "Album |||| Album",
|
||||
"fields": {
|
||||
"albumArtist": "Artis Album",
|
||||
"artist": "Artis",
|
||||
"duration": "Durasi",
|
||||
"songCount": "Lagu",
|
||||
"playCount": "Dimainkan",
|
||||
"name": "Nama",
|
||||
"genre": "Genre",
|
||||
"compilation": "Kompilasi",
|
||||
"year": "Tahun",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"comment": "Komentar",
|
||||
"rating": "Peringkat",
|
||||
"createdAt": "Tgl. Ditambahkan",
|
||||
"size": "Ukuran",
|
||||
"originalDate": "Tanggal",
|
||||
"releaseDate": "Rilis",
|
||||
"releases": "Rilis |||| Rilis",
|
||||
"released": "Dirilis"
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "Mainkan",
|
||||
"playNext": "Mainkan selanjutnya",
|
||||
"addToQueue": "Tambah ke antrean",
|
||||
"shuffle": "Acak",
|
||||
"addToPlaylist": "Tambahkan ke Playlist",
|
||||
"download": "Unduh",
|
||||
"info": "Lihat Info",
|
||||
"share": "Bagikan"
|
||||
},
|
||||
"lists": {
|
||||
"all": "Semua",
|
||||
"random": "Acak",
|
||||
"recentlyAdded": "Terakhir Ditambahkan",
|
||||
"recentlyPlayed": "Terakhir Dimainkan",
|
||||
"mostPlayed": "Sering Dimainkan",
|
||||
"starred": "Favorit",
|
||||
"topRated": "Peringkat Teratas"
|
||||
}
|
||||
},
|
||||
"artist": {
|
||||
"name": "Artis |||| Artis",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"albumCount": "Jumlah Album",
|
||||
"songCount": "Jumlah Lagu",
|
||||
"playCount": "Dimainkan",
|
||||
"rating": "Peringkat",
|
||||
"genre": "Genre",
|
||||
"size": "Ukuran"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"name": "Pengguna |||| Pengguna",
|
||||
"fields": {
|
||||
"userName": "Nama Pengguna",
|
||||
"isAdmin": "Admin",
|
||||
"lastLoginAt": "Terakhir Login",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"name": "Nama",
|
||||
"password": "Kata Sandi",
|
||||
"createdAt": "Dibuat pada",
|
||||
"changePassword": "Ganti Kata Sandi?",
|
||||
"currentPassword": "Kata Sandi Sebelumnya",
|
||||
"newPassword": "Kata Sandi Baru",
|
||||
"token": "Token"
|
||||
},
|
||||
"helperTexts": {
|
||||
"name": "Perubahan pada nama Kamu akan terlihat pada login berikutnya"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Pengguna dibuat",
|
||||
"updated": "Pengguna diperbarui",
|
||||
"deleted": "Pengguna dihapus"
|
||||
},
|
||||
"message": {
|
||||
"listenBrainzToken": "Masukkan token pengguna ListenBrainz Kamu.",
|
||||
"clickHereForToken": "Klik di sini untuk mendapatkan token ListenBrainz"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"name": "Pemutar |||| Pemutar",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"transcodingId": "Transkode",
|
||||
"maxBitRate": "Maks. Laju Bit",
|
||||
"client": "Klien",
|
||||
"userName": "Nama Pengguna",
|
||||
"lastSeen": "Terakhir Terlihat Pada",
|
||||
"reportRealPath": "Laporkan Jalur Sebenarnya",
|
||||
"scrobbleEnabled": "Kirim Scrobbles ke layanan eksternal"
|
||||
}
|
||||
},
|
||||
"transcoding": {
|
||||
"name": "Transkode |||| Transkode",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"targetFormat": "Target Format",
|
||||
"defaultBitRate": "Laju Bit Bawaan",
|
||||
"command": "Perintah"
|
||||
}
|
||||
},
|
||||
"playlist": {
|
||||
"name": "Playlist |||| Playlist",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"duration": "Durasi",
|
||||
"ownerName": "Pemilik",
|
||||
"public": "Publik",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"createdAt": "Dibuat pada",
|
||||
"songCount": "Lagu",
|
||||
"comment": "Komentar",
|
||||
"sync": "Impor Otomatis",
|
||||
"path": "Impor Dari"
|
||||
},
|
||||
"actions": {
|
||||
"selectPlaylist": "Pilih playlist:",
|
||||
"addNewPlaylist": "Buat \"%{name}\"",
|
||||
"export": "Ekspor",
|
||||
"makePublic": "Jadikan Publik",
|
||||
"makePrivate": "Jadikan Pribadi"
|
||||
},
|
||||
"message": {
|
||||
"duplicate_song": "Tambahkan lagu duplikat",
|
||||
"song_exist": "Ada lagu duplikat yang ditambahkan ke daftar putar. Apakah Kamu ingin menambahkan lagu duplikat atau melewatkannya?"
|
||||
}
|
||||
},
|
||||
"radio": {
|
||||
"name": "Radio |||| Radio",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"streamUrl": "URL Sumber",
|
||||
"homePageUrl": "Halaman Beranda URL",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"createdAt": "Dibuat pada"
|
||||
},
|
||||
"actions": {
|
||||
"playNow": "Mainkan sekarang"
|
||||
}
|
||||
},
|
||||
"share": {
|
||||
"name": "Bagikan |||| Bagikan",
|
||||
"fields": {
|
||||
"username": "Dibagikan Oleh",
|
||||
"url": "URL",
|
||||
"description": "Deskripsi",
|
||||
"contents": "Konten",
|
||||
"expiresAt": "Berakhir",
|
||||
"lastVisitedAt": "Terakhir Dikunjungi",
|
||||
"visitCount": "Pengunjung",
|
||||
"format": "Format",
|
||||
"maxBitRate": "Maks. Laju Bit",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"createdAt": "Dibuat pada",
|
||||
"downloadable": "Izinkan Pengunduhan?"
|
||||
}
|
||||
}
|
||||
"languageName": "Bahasa Indonesia",
|
||||
"resources": {
|
||||
"song": {
|
||||
"name": "Lagu |||| Lagu",
|
||||
"fields": {
|
||||
"albumArtist": "Artis Album",
|
||||
"duration": "Durasi",
|
||||
"trackNumber": "#",
|
||||
"playCount": "Diputar",
|
||||
"title": "Judul",
|
||||
"artist": "Artis",
|
||||
"album": "Album",
|
||||
"path": "Lokasi file",
|
||||
"genre": "Genre",
|
||||
"compilation": "Kompilasi",
|
||||
"year": "Tahun",
|
||||
"size": "Ukuran file",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"bitRate": "Bit rate",
|
||||
"discSubtitle": "Subtitle Disk",
|
||||
"starred": "Favorit",
|
||||
"comment": "Komentar",
|
||||
"rating": "Peringkat",
|
||||
"quality": "Kualitas",
|
||||
"bpm": "BPM",
|
||||
"playDate": "Terakhir Diputar",
|
||||
"channels": "Saluran",
|
||||
"createdAt": "Tgl. Ditambahkan",
|
||||
"grouping": "Mengelompokkan",
|
||||
"mood": "Mood",
|
||||
"participants": "Partisipan tambahan",
|
||||
"tags": "Tag tambahan",
|
||||
"mappedTags": "Tag yang dipetakan",
|
||||
"rawTags": "Tag raw"
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "Tambah ke antrean",
|
||||
"playNow": "Putar sekarang",
|
||||
"addToPlaylist": "Tambahkan ke Playlist",
|
||||
"shuffleAll": "Acak Semua",
|
||||
"download": "Unduh",
|
||||
"playNext": "Putar Berikutnya",
|
||||
"info": "Lihat Info"
|
||||
}
|
||||
},
|
||||
"ra": {
|
||||
"auth": {
|
||||
"welcome1": "Terima kasih telah menginstal Navidrome!",
|
||||
"welcome2": "Untuk memulai, buat dulu akun admin",
|
||||
"confirmPassword": "Konfirmasi Kata Sandi",
|
||||
"buttonCreateAdmin": "Buat Akun Admin",
|
||||
"auth_check_error": "Silahkan masuk untuk melanjutkan",
|
||||
"user_menu": "Profil",
|
||||
"username": "Nama Pengguna",
|
||||
"password": "Kata Sandi",
|
||||
"sign_in": "Masuk",
|
||||
"sign_in_error": "Otentikasi gagal, silakan coba lagi",
|
||||
"logout": "Keluar"
|
||||
},
|
||||
"validation": {
|
||||
"invalidChars": "Harap menggunakan huruf dan angka saja",
|
||||
"passwordDoesNotMatch": "Kata sandi tidak cocok",
|
||||
"required": "Wajib",
|
||||
"minLength": "Setidaknya harus %{min} karakter",
|
||||
"maxLength": "Harus berisi %{max} karakter atau kurang",
|
||||
"minValue": "Minimal harus %{min}",
|
||||
"maxValue": "Harus %{max} atau kurang",
|
||||
"number": "Harus berupa angka",
|
||||
"email": "Harus berupa email yang valid",
|
||||
"oneOf": "Harus salah satu dari: %{options}",
|
||||
"regex": "Harus cocok dengan format spesifik (regexp): %{pattern}",
|
||||
"unique": "Harus unik",
|
||||
"url": "Harus berupa URL yang valid"
|
||||
},
|
||||
"action": {
|
||||
"add_filter": "Tambah filter",
|
||||
"add": "Tambah",
|
||||
"back": "Kembali",
|
||||
"bulk_actions": "1 item dipilih |||| %{smart_count} item dipilih",
|
||||
"cancel": "Batalkan",
|
||||
"clear_input_value": "Hapus",
|
||||
"clone": "Klon",
|
||||
"confirm": "Konfirmasi",
|
||||
"create": "Buat",
|
||||
"delete": "Hapus",
|
||||
"edit": "Edit",
|
||||
"export": "Ekspor",
|
||||
"list": "Daftar",
|
||||
"refresh": "Refresh",
|
||||
"remove_filter": "Hapus filter ini",
|
||||
"remove": "Hapus",
|
||||
"save": "Simpan",
|
||||
"search": "Cari",
|
||||
"show": "Tunjukkan",
|
||||
"sort": "Sortir",
|
||||
"undo": "Batalkan",
|
||||
"expand": "Luaskan",
|
||||
"close": "Tutup",
|
||||
"open_menu": "Buka menu",
|
||||
"close_menu": "Tutup menu",
|
||||
"unselect": "Batalkan pilihan",
|
||||
"skip": "Lewati",
|
||||
"bulk_actions_mobile": "1 |||| %{smart_count}",
|
||||
"share": "Bagikan",
|
||||
"download": "Unduh"
|
||||
},
|
||||
"boolean": {
|
||||
"true": "Ya",
|
||||
"false": "Tidak"
|
||||
},
|
||||
"page": {
|
||||
"create": "Buat %{name}",
|
||||
"dashboard": "Dashboard",
|
||||
"edit": "%{name} #%{id}",
|
||||
"error": "Ada yang tidak beres",
|
||||
"list": "%{name}",
|
||||
"loading": "Memuat",
|
||||
"not_found": "Tidak ditemukan",
|
||||
"show": "%{name} #%{id}",
|
||||
"empty": "Belum ada %{name}.",
|
||||
"invite": "Apakah Kamu ingin menambahkan satu?"
|
||||
},
|
||||
"input": {
|
||||
"file": {
|
||||
"upload_several": "Letakkan beberapa file untuk diunggah, atau klik untuk memilih salah satu.",
|
||||
"upload_single": "Letakkan file untuk diunggah, atau klik untuk memilihnya."
|
||||
},
|
||||
"image": {
|
||||
"upload_several": "Letakkan beberapa gambar untuk diunggah, atau klik untuk memilih salah satu.",
|
||||
"upload_single": "Letakkan gambar untuk diunggah, atau klik untuk memilihnya."
|
||||
},
|
||||
"references": {
|
||||
"all_missing": "Tidak dapat menemukan data referensi.",
|
||||
"many_missing": "Tampaknya beberapa referensi tidak tersedia.",
|
||||
"single_missing": "Tampaknya referensi tidak tersedia."
|
||||
},
|
||||
"password": {
|
||||
"toggle_visible": "Sembunyikan Kata Sandi",
|
||||
"toggle_hidden": "Tampilkan Kata Sandi"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"about": "Tentang",
|
||||
"are_you_sure": "Kamu Yakin?",
|
||||
"bulk_delete_content": "Kamu yakin ingin menghapus %{name} ini? |||| Kamu yakin ingin menghapus %{smart_count} item ini?",
|
||||
"bulk_delete_title": "Hapus %{name} |||| Hapus %{smart_count} %{name}",
|
||||
"delete_content": "Kamu ingin menghapus item ini?",
|
||||
"delete_title": "Hapus %{name} #%{id}",
|
||||
"details": "Detail",
|
||||
"error": "Terjadi kesalahan klien dan permintaan Kamu tidak dapat diselesaikan.",
|
||||
"invalid_form": "Formulirnya tidak valid. Silakan periksa kesalahannya",
|
||||
"loading": "Halaman sedang dimuat, mohon tunggu sebentar",
|
||||
"no": "Tidak",
|
||||
"not_found": "Mungkin Kamu mengetik URL yang salah, atau Kamu mengikuti tautan yang buruk.",
|
||||
"yes": "Ya",
|
||||
"unsaved_changes": "Beberapa perubahan tidak disimpan. Apakah Kamu yakin ingin mengabaikannya?"
|
||||
},
|
||||
"navigation": {
|
||||
"no_results": "Tidak ada hasil yang ditemukan",
|
||||
"no_more_results": "Nomor halaman %{page} melampaui batas. Coba halaman sebelumnya.",
|
||||
"page_out_of_boundaries": "Nomor halaman %{page} melampaui batas",
|
||||
"page_out_from_end": "Tidak dapat menelusuri sebelum halaman terakhir",
|
||||
"page_out_from_begin": "Tidak dapat menelusuri sebelum halaman 1",
|
||||
"page_range_info": "%{offsetBegin}-%{offsetEnd} dari %{total}",
|
||||
"page_rows_per_page": "Item per halaman:",
|
||||
"next": "Selanjutnya",
|
||||
"prev": "Sebelumnya",
|
||||
"skip_nav": "Lewati ke konten"
|
||||
},
|
||||
"notification": {
|
||||
"updated": "Elemen diperbarui |||| %{smart_count} elemen diperbarui",
|
||||
"created": "Elemen dibuat",
|
||||
"deleted": "Elemen dihapus |||| %{smart_count} elemen dihapus",
|
||||
"bad_item": "Elemen salah",
|
||||
"item_doesnt_exist": "Tidak ada elemen",
|
||||
"http_error": "Kesalahan komunikasi server",
|
||||
"data_provider_error": "dataProvider galat. Periksa konsol untuk detailnya.",
|
||||
"i18n_error": "Tidak dapat memuat terjemahan untuk bahasa yang diatur",
|
||||
"canceled": "Tindakan dibatalkan",
|
||||
"logged_out": "Sesi Kamu telah berakhir, harap sambungkan kembali.",
|
||||
"new_version": "Tersedia versi baru! Silakan menyegarkan jendela ini."
|
||||
},
|
||||
"toggleFieldsMenu": {
|
||||
"columnsToDisplay": "Kolom Untuk Ditampilkan",
|
||||
"layout": "Layout",
|
||||
"grid": "Grid",
|
||||
"table": "Tabel"
|
||||
}
|
||||
"album": {
|
||||
"name": "Album |||| Album",
|
||||
"fields": {
|
||||
"albumArtist": "Artis Album",
|
||||
"artist": "Artis",
|
||||
"duration": "Durasi",
|
||||
"songCount": "Lagu",
|
||||
"playCount": "Diputar",
|
||||
"name": "Nama",
|
||||
"genre": "Genre",
|
||||
"compilation": "Kompilasi",
|
||||
"year": "Tahun",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"comment": "Komentar",
|
||||
"rating": "Peringkat",
|
||||
"createdAt": "Tgl. Ditambahkan",
|
||||
"size": "Ukuran",
|
||||
"originalDate": "Tanggal",
|
||||
"releaseDate": "Dirilis",
|
||||
"releases": "Rilis |||| Rilis",
|
||||
"released": "Dirilis",
|
||||
"recordLabel": "Label",
|
||||
"catalogNum": "Nomer Katalog",
|
||||
"releaseType": "Tipe",
|
||||
"grouping": "Pengelompokkan",
|
||||
"media": "Media",
|
||||
"mood": "Mood"
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "Putar",
|
||||
"playNext": "Putar Selanjutnya",
|
||||
"addToQueue": "Putar Nanti",
|
||||
"shuffle": "Acak",
|
||||
"addToPlaylist": "Tambahkan ke Playlist",
|
||||
"download": "Unduh",
|
||||
"info": "Lihat Info",
|
||||
"share": "Bagikan"
|
||||
},
|
||||
"lists": {
|
||||
"all": "Semua",
|
||||
"random": "Acak",
|
||||
"recentlyAdded": "Terakhir Ditambahkan",
|
||||
"recentlyPlayed": "Terakhir Diputar",
|
||||
"mostPlayed": "Sering Diputar",
|
||||
"starred": "Favorit",
|
||||
"topRated": "Peringkat Teratas"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"note": "CATATAN",
|
||||
"transcodingDisabled": "Mengubah konfigurasi transkode melalui antarmuka web dinonaktifkan karena alasan keamanan. Jika Kamu ingin mengubah (mengedit atau menambahkan) opsi transkode, restart server dengan opsi konfigurasi %{config}.",
|
||||
"transcodingEnabled": "Navidrome saat ini berjalan dengan %{config}, sehingga memungkinkan untuk menjalankan perintah sistem dari pengaturan Transkode menggunakan antarmuka web. Kami sarankan untuk menonaktifkannya demi alasan keamanan dan hanya mengaktifkannya saat mengonfigurasi opsi Transcoding.",
|
||||
"songsAddedToPlaylist": "Menambahkan 1 lagu ke playlist |||| Menambahkan %{smart_count} lagu ke playlist",
|
||||
"noPlaylistsAvailable": "Tidak tersedia",
|
||||
"delete_user_title": "Hapus pengguna '%{name}'",
|
||||
"delete_user_content": "Apakah Kamu yakin ingin menghapus pengguna ini dan semua datanya (termasuk daftar putar dan preferensi)?",
|
||||
"notifications_blocked": "Kamu telah memblokir Notifikasi untuk situs ini di pengaturan browser Anda",
|
||||
"notifications_not_available": "Browser ini tidak mendukung notifikasi desktop atau Kamu tidak mengakses Navidrome melalui https",
|
||||
"lastfmLinkSuccess": "Last.fm berhasil ditautkan dan scrobbling diaktifkan",
|
||||
"lastfmLinkFailure": "Last.fm tidak dapat ditautkan",
|
||||
"lastfmUnlinkSuccess": "Tautan Last.fm dibatalkan dan scrobbling dinonaktifkan",
|
||||
"lastfmUnlinkFailure": "Tautan Last.fm tidak dapat dibatalkan",
|
||||
"openIn": {
|
||||
"lastfm": "Lihat di Last.fm",
|
||||
"musicbrainz": "Lihat di MusicBrainz"
|
||||
},
|
||||
"lastfmLink": "Baca selengkapnya...",
|
||||
"listenBrainzLinkSuccess": "ListenBrainz berhasil ditautkan dan scrobbling diaktifkan sebagai pengguna: %{user}",
|
||||
"listenBrainzLinkFailure": "ListenBrainz tidak dapat ditautkan: %{error}",
|
||||
"listenBrainzUnlinkSuccess": "Tautan ListenBrainz dibatalkan dan scrobbling dinonaktifkan",
|
||||
"listenBrainzUnlinkFailure": "Tautan ListenBrainz tidak dapat dibatalkan",
|
||||
"downloadOriginalFormat": "Unduh dalam format asli",
|
||||
"shareOriginalFormat": "Bagikan dalam format asli",
|
||||
"shareDialogTitle": "Bagikan %{resource} '%{name}'",
|
||||
"shareBatchDialogTitle": "Bagikan 1 %{resource} |||| Bagikan %{smart_count} %{resource}",
|
||||
"shareSuccess": "URL disalin ke papan klip: %{url}",
|
||||
"shareFailure": "Terjadi kesalahan saat menyalin URL %{url} ke papan klip",
|
||||
"downloadDialogTitle": "Unduh %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "Salin ke papan klip: Ctrl+C, Enter"
|
||||
"artist": {
|
||||
"name": "Artis |||| Artis",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"albumCount": "Jumlah Album",
|
||||
"songCount": "Jumlah Lagu",
|
||||
"playCount": "Diputar",
|
||||
"rating": "Peringkat",
|
||||
"genre": "Genre",
|
||||
"size": "Ukuran",
|
||||
"role": "Peran"
|
||||
},
|
||||
"roles": {
|
||||
"albumartist": "Artis Album |||| Artis Album",
|
||||
"artist": "Artis |||| Artis",
|
||||
"composer": "Komposer |||| Komposer",
|
||||
"conductor": "Konduktor |||| Konduktor",
|
||||
"lyricist": "Penulis Lirik |||| Penulis Lirik",
|
||||
"arranger": "Arranger |||| Arranger",
|
||||
"producer": "Produser |||| Produser",
|
||||
"director": "Director |||| Director",
|
||||
"engineer": "Engineer |||| Engineer",
|
||||
"mixer": "Mixer |||| Mixer",
|
||||
"remixer": "Remixer |||| Remixer",
|
||||
"djmixer": "DJ Mixer |||| Dj Mixer",
|
||||
"performer": "Performer |||| Performer"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"library": "Perpustakaan",
|
||||
"settings": "Pengaturan",
|
||||
"version": "Versi",
|
||||
"theme": "Tema",
|
||||
"personal": {
|
||||
"name": "Personal",
|
||||
"options": {
|
||||
"theme": "Tema",
|
||||
"language": "Bahasa",
|
||||
"defaultView": "Tampilan Bawaan",
|
||||
"desktop_notifications": "Pemberitahuan Desktop",
|
||||
"lastfmScrobbling": "Scrobble ke Last.fm",
|
||||
"listenBrainzScrobbling": "Scrobble ke ListenBrainz",
|
||||
"replaygain": "Mode ReplayGain",
|
||||
"preAmp": "ReplayGain PreAmp (dB)",
|
||||
"gain": {
|
||||
"none": "Nonaktif",
|
||||
"album": "Gunakan Gain Album",
|
||||
"track": "Gunakan Gain Lagu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"albumList": "Album",
|
||||
"about": "Tentang",
|
||||
"playlists": "Playlist",
|
||||
"sharedPlaylists": "Playlist yang Dibagikan"
|
||||
"user": {
|
||||
"name": "Pengguna |||| Pengguna",
|
||||
"fields": {
|
||||
"userName": "Nama Pengguna",
|
||||
"isAdmin": "Admin",
|
||||
"lastLoginAt": "Terakhir Login",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"name": "Nama",
|
||||
"password": "Kata Sandi",
|
||||
"createdAt": "Dibuat pada",
|
||||
"changePassword": "Ganti Kata Sandi?",
|
||||
"currentPassword": "Kata Sandi Sebelumnya",
|
||||
"newPassword": "Kata Sandi Baru",
|
||||
"token": "Token",
|
||||
"lastAccessAt": "Terakhir Diakses"
|
||||
},
|
||||
"helperTexts": {
|
||||
"name": "Perubahan pada nama Kamu akan terlihat pada login berikutnya"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Pengguna dibuat",
|
||||
"updated": "Pengguna diperbarui",
|
||||
"deleted": "Pengguna dihapus"
|
||||
},
|
||||
"message": {
|
||||
"listenBrainzToken": "Masukkan token pengguna ListenBrainz Kamu.",
|
||||
"clickHereForToken": "Klik di sini untuk mendapatkan token baru anda"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"playListsText": "Mainkan Antrean",
|
||||
"openText": "Buka text",
|
||||
"closeText": "Tutup text",
|
||||
"notContentText": "Tidak ada musik",
|
||||
"clickToPlayText": "Klik untuk mainkan",
|
||||
"clickToPauseText": "Klik untuk menjeda",
|
||||
"nextTrackText": "Lagu Selanjutnya",
|
||||
"previousTrackText": "Lagu Sebelumnya",
|
||||
"reloadText": "Muat ulang",
|
||||
"volumeText": "Volume",
|
||||
"toggleLyricText": "Lirik",
|
||||
"toggleMiniModeText": "Minimalkan",
|
||||
"destroyText": "Tutup",
|
||||
"downloadText": "Unduh",
|
||||
"removeAudioListsText": "Hapus daftar audio",
|
||||
"clickToDeleteText": "Klik untuk menghapus %{name}",
|
||||
"emptyLyricText": "Tidak ada lirik",
|
||||
"playModeText": {
|
||||
"order": "Berurutan",
|
||||
"orderLoop": "Ulang",
|
||||
"singleLoop": "Ulangi Satu",
|
||||
"shufflePlay": "Acak"
|
||||
}
|
||||
"name": "Pemutar |||| Pemutar",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"transcodingId": "Transkode",
|
||||
"maxBitRate": "Maks. Bit Rate",
|
||||
"client": "Klien",
|
||||
"userName": "Nama Pengguna",
|
||||
"lastSeen": "Terakhir Terlihat Pada",
|
||||
"reportRealPath": "Laporkan Jalur Sebenarnya",
|
||||
"scrobbleEnabled": "Kirim Scrobbles ke layanan eksternal"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"links": {
|
||||
"homepage": "Halaman beranda",
|
||||
"source": "Kode sumber",
|
||||
"featureRequests": "Permintaan fitur"
|
||||
}
|
||||
"transcoding": {
|
||||
"name": "Transkode |||| Transkode",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"targetFormat": "Target Format",
|
||||
"defaultBitRate": "Bit Rate Bawaan",
|
||||
"command": "Perintah"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"title": "Aktivitas",
|
||||
"totalScanned": "Total Folder yang Dipindai",
|
||||
"quickScan": "Pemindaian Cepat",
|
||||
"fullScan": "Pemindaian Penuh",
|
||||
"serverUptime": "Waktu Aktif Server",
|
||||
"serverDown": "OFFLINE"
|
||||
"playlist": {
|
||||
"name": "Playlist |||| Playlist",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"duration": "Durasi",
|
||||
"ownerName": "Pemilik",
|
||||
"public": "Publik",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"createdAt": "Dibuat pada",
|
||||
"songCount": "Lagu",
|
||||
"comment": "Komentar",
|
||||
"sync": "Impor Otomatis",
|
||||
"path": "Impor Dari"
|
||||
},
|
||||
"actions": {
|
||||
"selectPlaylist": "Pilih playlist:",
|
||||
"addNewPlaylist": "Buat \"%{name}\"",
|
||||
"export": "Ekspor",
|
||||
"makePublic": "Jadikan Publik",
|
||||
"makePrivate": "Jadikan Pribadi"
|
||||
},
|
||||
"message": {
|
||||
"duplicate_song": "Tambahkan lagu duplikat",
|
||||
"song_exist": "Ada lagu duplikat yang ditambahkan ke daftar putar. Apakah Kamu ingin menambahkan lagu duplikat atau melewatkannya?"
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"title": "Tombol Pintasan Navidrome",
|
||||
"hotkeys": {
|
||||
"show_help": "Tampilkan Bantuan Ini",
|
||||
"toggle_menu": "Menu Samping",
|
||||
"toggle_play": "Mainkan / Jeda",
|
||||
"prev_song": "Lagu Sebelumnya",
|
||||
"next_song": "Lagu Selanjutnya",
|
||||
"vol_up": "Volume Naik",
|
||||
"vol_down": "Volume Turun",
|
||||
"toggle_love": "Tambahkan lagu ini ke favorit",
|
||||
"current_song": "Buka Lagu Saat Ini"
|
||||
}
|
||||
"radio": {
|
||||
"name": "Radio |||| Radio",
|
||||
"fields": {
|
||||
"name": "Nama",
|
||||
"streamUrl": "URL Stream",
|
||||
"homePageUrl": "Halaman Beranda URL",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"createdAt": "Dibuat pada"
|
||||
},
|
||||
"actions": {
|
||||
"playNow": "Putar Sekarang"
|
||||
}
|
||||
},
|
||||
"share": {
|
||||
"name": "Bagikan |||| Bagikan",
|
||||
"fields": {
|
||||
"username": "Dibagikan Oleh",
|
||||
"url": "URL",
|
||||
"description": "Deskripsi",
|
||||
"contents": "Konten",
|
||||
"expiresAt": "Berakhir",
|
||||
"lastVisitedAt": "Terakhir Dikunjungi",
|
||||
"visitCount": "Pengunjung",
|
||||
"format": "Format",
|
||||
"maxBitRate": "Maks. Laju Bit",
|
||||
"updatedAt": "Diperbarui pada",
|
||||
"createdAt": "Dibuat pada",
|
||||
"downloadable": "Izinkan Pengunduhan?"
|
||||
}
|
||||
},
|
||||
"missing": {
|
||||
"name": "File yang Hilang |||| File yang Hilang",
|
||||
"fields": {
|
||||
"path": "Jalur",
|
||||
"size": "Ukuran",
|
||||
"updatedAt": "Tidak muncul di"
|
||||
},
|
||||
"actions": {
|
||||
"remove": "Hapus"
|
||||
},
|
||||
"notifications": {
|
||||
"removed": "File yang hilang dihapus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ra": {
|
||||
"auth": {
|
||||
"welcome1": "Terima kasih telah menginstal Navidrome!",
|
||||
"welcome2": "Untuk memulai, buat dulu akun admin",
|
||||
"confirmPassword": "Konfirmasi Kata Sandi",
|
||||
"buttonCreateAdmin": "Buat Akun Admin",
|
||||
"auth_check_error": "Silahkan masuk untuk melanjutkan",
|
||||
"user_menu": "Profil",
|
||||
"username": "Nama Pengguna",
|
||||
"password": "Kata Sandi",
|
||||
"sign_in": "Masuk",
|
||||
"sign_in_error": "Otentikasi gagal, silakan coba lagi",
|
||||
"logout": "Keluar",
|
||||
"insightsCollectionNote": "Navidrome mengumpulkan penggunaan data anonim untuk membantu menyempurnakan project ini. Klik [disini] untuk mempelajari lebih lanjut dan untuk opt-out jika anda mau"
|
||||
},
|
||||
"validation": {
|
||||
"invalidChars": "Harap menggunakan huruf dan angka saja",
|
||||
"passwordDoesNotMatch": "Kata sandi tidak cocok",
|
||||
"required": "Wajib",
|
||||
"minLength": "Setidaknya harus %{min} karakter",
|
||||
"maxLength": "Harus berisi %{max} karakter atau kurang",
|
||||
"minValue": "Minimal harus %{min}",
|
||||
"maxValue": "Harus %{max} atau kurang",
|
||||
"number": "Harus berupa angka",
|
||||
"email": "Harus berupa email yang valid",
|
||||
"oneOf": "Harus salah satu dari: %{options}",
|
||||
"regex": "Harus cocok dengan format spesifik (regexp): %{pattern}",
|
||||
"unique": "Harus unik",
|
||||
"url": "Harus berupa URL yang valid"
|
||||
},
|
||||
"action": {
|
||||
"add_filter": "Tambah filter",
|
||||
"add": "Tambah",
|
||||
"back": "Kembali",
|
||||
"bulk_actions": "1 item dipilih |||| %{smart_count} item dipilih",
|
||||
"cancel": "Batalkan",
|
||||
"clear_input_value": "Hapus",
|
||||
"clone": "Klon",
|
||||
"confirm": "Konfirmasi",
|
||||
"create": "Buat",
|
||||
"delete": "Hapus",
|
||||
"edit": "Sunting",
|
||||
"export": "Ekspor",
|
||||
"list": "Daftar",
|
||||
"refresh": "Segarkan",
|
||||
"remove_filter": "Hapus filter ini",
|
||||
"remove": "Hapus",
|
||||
"save": "Simpan",
|
||||
"search": "Cari",
|
||||
"show": "Tampilkan",
|
||||
"sort": "Sortir",
|
||||
"undo": "Batalkan",
|
||||
"expand": "Luaskan",
|
||||
"close": "Tutup",
|
||||
"open_menu": "Buka menu",
|
||||
"close_menu": "Tutup menu",
|
||||
"unselect": "Batalkan pilihan",
|
||||
"skip": "Lewati",
|
||||
"bulk_actions_mobile": "1 |||| %{smart_count}",
|
||||
"share": "Bagikan",
|
||||
"download": "Unduh"
|
||||
},
|
||||
"boolean": {
|
||||
"true": "Ya",
|
||||
"false": "Tidak"
|
||||
},
|
||||
"page": {
|
||||
"create": "Buat %{name}",
|
||||
"dashboard": "Dasbor",
|
||||
"edit": "%{name} #%{id}",
|
||||
"error": "Ada yang tidak beres",
|
||||
"list": "%{name}",
|
||||
"loading": "Memuat",
|
||||
"not_found": "Tidak ditemukan",
|
||||
"show": "%{name} #%{id}",
|
||||
"empty": "Belum ada %{name}.",
|
||||
"invite": "Apakah kamu ingin menambahkan satu?"
|
||||
},
|
||||
"input": {
|
||||
"file": {
|
||||
"upload_several": "Letakkan beberapa file untuk diunggah, atau klik untuk memilih salah satu.",
|
||||
"upload_single": "Letakkan file untuk diunggah, atau klik untuk memilihnya."
|
||||
},
|
||||
"image": {
|
||||
"upload_several": "Letakkan beberapa gambar untuk diunggah, atau klik untuk memilih salah satu.",
|
||||
"upload_single": "Letakkan gambar untuk diunggah, atau klik untuk memilihnya."
|
||||
},
|
||||
"references": {
|
||||
"all_missing": "Tidak dapat menemukan data referensi.",
|
||||
"many_missing": "Tampaknya beberapa referensi tidak tersedia.",
|
||||
"single_missing": "Referensi yang ter asosiasi tidak tersedia untuk ditampilkan."
|
||||
},
|
||||
"password": {
|
||||
"toggle_visible": "Sembunyikan kata sandi",
|
||||
"toggle_hidden": "Tampilkan kata sandi"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"about": "Tentang",
|
||||
"are_you_sure": "Kamu Yakin?",
|
||||
"bulk_delete_content": "Kamu yakin ingin menghapus %{name} ini? |||| Kamu yakin ingin menghapus %{smart_count} item ini?",
|
||||
"bulk_delete_title": "Hapus %{name} |||| Hapus %{smart_count} %{name}",
|
||||
"delete_content": "Kamu ingin menghapus item ini?",
|
||||
"delete_title": "Hapus %{name} #%{id}",
|
||||
"details": "Detail",
|
||||
"error": "Terjadi kesalahan klien dan permintaan Kamu tidak dapat diselesaikan.",
|
||||
"invalid_form": "Form tidak valid. Silakan periksa kesalahannya",
|
||||
"loading": "Halaman sedang dimuat, mohon tunggu sebentar",
|
||||
"no": "Tidak",
|
||||
"not_found": "Mungkin Kamu mengetik URL yang salah, atau Kamu mengikuti tautan yang buruk.",
|
||||
"yes": "Ya",
|
||||
"unsaved_changes": "Beberapa perubahan tidak disimpan. Apakah Kamu yakin ingin mengabaikannya?"
|
||||
},
|
||||
"navigation": {
|
||||
"no_results": "Tidak ada hasil yang ditemukan",
|
||||
"no_more_results": "Nomor halaman %{page} melampaui batas. Coba halaman sebelumnya.",
|
||||
"page_out_of_boundaries": "Nomor halaman %{page} melampaui batas",
|
||||
"page_out_from_end": "Tidak dapat menelusuri sebelum halaman terakhir",
|
||||
"page_out_from_begin": "Tidak dapat menelusuri sebelum halaman 1",
|
||||
"page_range_info": "%{offsetBegin}-%{offsetEnd} dari %{total}",
|
||||
"page_rows_per_page": "Item per halaman:",
|
||||
"next": "Selanjutnya",
|
||||
"prev": "Sebelumnya",
|
||||
"skip_nav": "Lewati ke konten"
|
||||
},
|
||||
"notification": {
|
||||
"updated": "Elemen diperbarui |||| %{smart_count} elemen diperbarui",
|
||||
"created": "Elemen dibuat",
|
||||
"deleted": "Elemen dihapus |||| %{smart_count} elemen dihapus",
|
||||
"bad_item": "Elemen salah",
|
||||
"item_doesnt_exist": "Tidak ada elemen",
|
||||
"http_error": "Kesalahan komunikasi peladen",
|
||||
"data_provider_error": "dataProvider galat. Periksa konsol untuk detailnya.",
|
||||
"i18n_error": "Tidak dapat memuat terjemahan untuk bahasa yang diatur",
|
||||
"canceled": "Tindakan dibatalkan",
|
||||
"logged_out": "Sesi Kamu telah berakhir, harap sambungkan kembali.",
|
||||
"new_version": "Tersedia versi baru! Silakan menyegarkan jendela ini."
|
||||
},
|
||||
"toggleFieldsMenu": {
|
||||
"columnsToDisplay": "Kolom Untuk Ditampilkan",
|
||||
"layout": "Tata Letak",
|
||||
"grid": "Ubin",
|
||||
"table": "Tabel"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"note": "CATATAN",
|
||||
"transcodingDisabled": "Mengubah konfigurasi transkode melalui antarmuka web dinonaktifkan karena alasan keamanan. Jika Kamu ingin mengubah (mengedit atau menambahkan) opsi transkode, restart server dengan opsi konfigurasi %{config}.",
|
||||
"transcodingEnabled": "Navidrome saat ini berjalan dengan %{config}, sehingga memungkinkan untuk menjalankan perintah sistem dari pengaturan Transkode menggunakan antarmuka web. Kami sarankan untuk menonaktifkannya demi alasan keamanan dan hanya mengaktifkannya saat mengonfigurasi opsi Transcoding.",
|
||||
"songsAddedToPlaylist": "Menambahkan 1 lagu ke playlist |||| Menambahkan %{smart_count} lagu ke playlist",
|
||||
"noPlaylistsAvailable": "Tidak tersedia",
|
||||
"delete_user_title": "Hapus pengguna '%{name}'",
|
||||
"delete_user_content": "Apakah Kamu yakin ingin menghapus pengguna ini dan semua datanya (termasuk daftar putar dan preferensi)?",
|
||||
"notifications_blocked": "Kamu telah memblokir Notifikasi untuk situs ini di pengaturan browser Anda",
|
||||
"notifications_not_available": "Browser ini tidak mendukung notifikasi desktop atau Kamu tidak mengakses Navidrome melalui https",
|
||||
"lastfmLinkSuccess": "Last.fm berhasil ditautkan dan scrobbling diaktifkan",
|
||||
"lastfmLinkFailure": "Last.fm tidak dapat ditautkan",
|
||||
"lastfmUnlinkSuccess": "Tautan Last.fm dibatalkan dan scrobbling dinonaktifkan",
|
||||
"lastfmUnlinkFailure": "Tautan Last.fm tidak dapat dibatalkan",
|
||||
"openIn": {
|
||||
"lastfm": "Lihat di Last.fm",
|
||||
"musicbrainz": "Lihat di MusicBrainz"
|
||||
},
|
||||
"lastfmLink": "Baca selengkapnya...",
|
||||
"listenBrainzLinkSuccess": "ListenBrainz berhasil ditautkan dan scrobbling diaktifkan sebagai pengguna: %{user}",
|
||||
"listenBrainzLinkFailure": "ListenBrainz tidak dapat ditautkan: %{error}",
|
||||
"listenBrainzUnlinkSuccess": "Tautan ListenBrainz dibatalkan dan scrobbling dinonaktifkan",
|
||||
"listenBrainzUnlinkFailure": "Tautan ListenBrainz tidak dapat dibatalkan",
|
||||
"downloadOriginalFormat": "Unduh dalam format asli",
|
||||
"shareOriginalFormat": "Bagikan dalam format asli",
|
||||
"shareDialogTitle": "Bagikan %{resource} '%{name}'",
|
||||
"shareBatchDialogTitle": "Bagikan 1 %{resource} |||| Bagikan %{smart_count} %{resource}",
|
||||
"shareSuccess": "URL disalin ke papan klip: %{url}",
|
||||
"shareFailure": "Terjadi kesalahan saat menyalin URL %{url} ke papan klip",
|
||||
"downloadDialogTitle": "Unduh %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "Salin ke papan klip: Ctrl+C, Enter",
|
||||
"remove_missing_title": "Hapus file yang hilang",
|
||||
"remove_missing_content": "Apakah Anda yakin ingin menghapus file-file yang hilang dari basis data? Tindakan ini akan menghapus secara permanen semua referensi ke file-file tersebut, termasuk jumlah pemutaran dan peringkatnya."
|
||||
},
|
||||
"menu": {
|
||||
"library": "Pustaka",
|
||||
"settings": "Pengaturan",
|
||||
"version": "Versi",
|
||||
"theme": "Tema",
|
||||
"personal": {
|
||||
"name": "Personal",
|
||||
"options": {
|
||||
"theme": "Tema",
|
||||
"language": "Bahasa",
|
||||
"defaultView": "Tampilan Bawaan",
|
||||
"desktop_notifications": "Pemberitahuan Desktop",
|
||||
"lastfmScrobbling": "Scrobble ke Last.fm",
|
||||
"listenBrainzScrobbling": "Scrobble ke ListenBrainz",
|
||||
"replaygain": "Mode ReplayGain",
|
||||
"preAmp": "ReplayGain PreAmp (dB)",
|
||||
"gain": {
|
||||
"none": "Nonaktif",
|
||||
"album": "Gunakan Gain Album",
|
||||
"track": "Gunakan Gain Lagu"
|
||||
},
|
||||
"lastfmNotConfigured": "API-Key Last.fm belum dikonfigurasi"
|
||||
}
|
||||
},
|
||||
"albumList": "Album",
|
||||
"about": "Tentang",
|
||||
"playlists": "Playlist",
|
||||
"sharedPlaylists": "Playlist yang Dibagikan"
|
||||
},
|
||||
"player": {
|
||||
"playListsText": "Mainkan Antrean",
|
||||
"openText": "Buka",
|
||||
"closeText": "Tutup",
|
||||
"notContentText": "Tidak ada musik",
|
||||
"clickToPlayText": "Klik untuk memutar",
|
||||
"clickToPauseText": "Klik untuk menjeda",
|
||||
"nextTrackText": "Lagu Selanjutnya",
|
||||
"previousTrackText": "Lagu Sebelumnya",
|
||||
"reloadText": "Muat ulang",
|
||||
"volumeText": "Volume",
|
||||
"toggleLyricText": "Lirik",
|
||||
"toggleMiniModeText": "Minimalkan",
|
||||
"destroyText": "Tutup",
|
||||
"downloadText": "Unduh",
|
||||
"removeAudioListsText": "Hapus daftar audio",
|
||||
"clickToDeleteText": "Klik untuk menghapus %{name}",
|
||||
"emptyLyricText": "Tidak ada lirik",
|
||||
"playModeText": {
|
||||
"order": "Berurutan",
|
||||
"orderLoop": "Ulang",
|
||||
"singleLoop": "Ulangi Satu",
|
||||
"shufflePlay": "Acak"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"links": {
|
||||
"homepage": "Halaman beranda",
|
||||
"source": "Kode sumber",
|
||||
"featureRequests": "Permintaan fitur",
|
||||
"lastInsightsCollection": "Koleksi insight terakhir",
|
||||
"insights": {
|
||||
"disabled": "Nonaktifkan",
|
||||
"waiting": "Menunggu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"title": "Aktivitas",
|
||||
"totalScanned": "Total Folder yang Dipindai",
|
||||
"quickScan": "Pemindaian Cepat",
|
||||
"fullScan": "Pemindaian Penuh",
|
||||
"serverUptime": "Waktu Aktif Peladen",
|
||||
"serverDown": "LURING"
|
||||
},
|
||||
"help": {
|
||||
"title": "Tombol Pintasan Navidrome",
|
||||
"hotkeys": {
|
||||
"show_help": "Tampilkan Bantuan Ini",
|
||||
"toggle_menu": "Menu Samping",
|
||||
"toggle_play": "Putar / Jeda",
|
||||
"prev_song": "Lagu Sebelumnya",
|
||||
"next_song": "Lagu Selanjutnya",
|
||||
"vol_up": "Volume Naik",
|
||||
"vol_down": "Volume Turun",
|
||||
"toggle_love": "Tambahkan lagu ini ke favorit",
|
||||
"current_song": "Buka Lagu Saat Ini"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,7 +26,13 @@
|
|||
"bpm": "BPM",
|
||||
"playDate": "最後の再生",
|
||||
"channels": "チャンネル",
|
||||
"createdAt": "追加日"
|
||||
"createdAt": "追加日",
|
||||
"grouping": "",
|
||||
"mood": "",
|
||||
"participants": "",
|
||||
"tags": "",
|
||||
"mappedTags": "",
|
||||
"rawTags": ""
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "最後に再生",
|
||||
|
@ -58,7 +64,13 @@
|
|||
"originalDate": "オリジナルの日付",
|
||||
"releaseDate": "リリース日",
|
||||
"releases": "リリース",
|
||||
"released": "リリース"
|
||||
"released": "リリース",
|
||||
"recordLabel": "",
|
||||
"catalogNum": "",
|
||||
"releaseType": "",
|
||||
"grouping": "",
|
||||
"media": "",
|
||||
"mood": ""
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "再生",
|
||||
|
@ -89,7 +101,23 @@
|
|||
"playCount": "再生数",
|
||||
"rating": "レート",
|
||||
"genre": "ジャンル",
|
||||
"size": "サイズ"
|
||||
"size": "サイズ",
|
||||
"role": ""
|
||||
},
|
||||
"roles": {
|
||||
"albumartist": "",
|
||||
"artist": "",
|
||||
"composer": "",
|
||||
"conductor": "",
|
||||
"lyricist": "",
|
||||
"arranger": "",
|
||||
"producer": "",
|
||||
"director": "",
|
||||
"engineer": "",
|
||||
"mixer": "",
|
||||
"remixer": "",
|
||||
"djmixer": "",
|
||||
"performer": ""
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
|
@ -198,6 +226,20 @@
|
|||
"createdAt": "作成日",
|
||||
"downloadable": "ダウンロードを許可しますか?"
|
||||
}
|
||||
},
|
||||
"missing": {
|
||||
"name": "",
|
||||
"fields": {
|
||||
"path": "",
|
||||
"size": "",
|
||||
"updatedAt": ""
|
||||
},
|
||||
"actions": {
|
||||
"remove": ""
|
||||
},
|
||||
"notifications": {
|
||||
"removed": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"ra": {
|
||||
|
@ -212,7 +254,8 @@
|
|||
"password": "パスワード",
|
||||
"sign_in": "ログイン",
|
||||
"sign_in_error": "認証に失敗しました。入力を確認してください",
|
||||
"logout": "ログアウト"
|
||||
"logout": "ログアウト",
|
||||
"insightsCollectionNote": "Navidromeでは、プロジェクトの改善に役立てるため、匿名の利用データを収集しています。詳しくは [here] をクリックしてください。"
|
||||
},
|
||||
"validation": {
|
||||
"invalidChars": "文字と数字のみを使用してください",
|
||||
|
@ -374,7 +417,9 @@
|
|||
"shareSuccess": "コピーしました: %{url}",
|
||||
"shareFailure": "コピーに失敗しました %{url}",
|
||||
"downloadDialogTitle": "ダウンロード %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "クリップボードへコピー: Ctrl+C, Enter"
|
||||
"shareCopyToClipboard": "クリップボードへコピー: Ctrl+C, Enter",
|
||||
"remove_missing_title": "",
|
||||
"remove_missing_content": ""
|
||||
},
|
||||
"menu": {
|
||||
"library": "ライブラリ",
|
||||
|
@ -396,7 +441,8 @@
|
|||
"none": "無効",
|
||||
"album": "アルバムゲインを使う",
|
||||
"track": "トラックゲインを使う"
|
||||
}
|
||||
},
|
||||
"lastfmNotConfigured": "Last.fmのAPIキーが設定されていません"
|
||||
}
|
||||
},
|
||||
"albumList": "アルバム",
|
||||
|
@ -433,7 +479,12 @@
|
|||
"links": {
|
||||
"homepage": "ホームページ",
|
||||
"source": "ソースコード",
|
||||
"featureRequests": "機能リクエスト"
|
||||
"featureRequests": "機能リクエスト",
|
||||
"lastInsightsCollection": "最後のデータ収集",
|
||||
"insights": {
|
||||
"disabled": "無効",
|
||||
"waiting": "待機中"
|
||||
}
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
|
|
|
@ -30,7 +30,9 @@
|
|||
"grouping": "Agrupamento",
|
||||
"mood": "Mood",
|
||||
"participants": "Outros Participantes",
|
||||
"tags": "Outras Tags"
|
||||
"tags": "Outras Tags",
|
||||
"mappedTags": "Tags mapeadas",
|
||||
"rawTags": "Tags originais"
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "Adicionar à fila",
|
||||
|
@ -124,7 +126,6 @@
|
|||
"userName": "Usuário",
|
||||
"isAdmin": "Admin?",
|
||||
"lastLoginAt": "Últ. Login",
|
||||
"lastAccessAt": "Últ. Acesso",
|
||||
"updatedAt": "Últ. Atualização",
|
||||
"name": "Nome",
|
||||
"password": "Senha",
|
||||
|
@ -132,7 +133,8 @@
|
|||
"changePassword": "Trocar Senha?",
|
||||
"currentPassword": "Senha Atual",
|
||||
"newPassword": "Nova Senha",
|
||||
"token": "Token"
|
||||
"token": "Token",
|
||||
"lastAccessAt": "Últ. Acesso"
|
||||
},
|
||||
"helperTexts": {
|
||||
"name": "Alterações no seu nome só serão refletidas no próximo login"
|
||||
|
@ -393,8 +395,6 @@
|
|||
"noPlaylistsAvailable": "Nenhuma playlist",
|
||||
"delete_user_title": "Excluir usuário '%{name}'",
|
||||
"delete_user_content": "Você tem certeza que deseja excluir o usuário e todos os seus dados (incluindo suas playlists e preferências)?",
|
||||
"remove_missing_title": "Remover arquivos ausentes",
|
||||
"remove_missing_content": "Você tem certeza que deseja remover os arquivos selecionados do banco de dados? Isso removerá permanentemente qualquer referência a eles, incluindo suas contagens de reprodução e classificações.",
|
||||
"notifications_blocked": "Você bloqueou notificações para este site nas configurações do seu browser",
|
||||
"notifications_not_available": "Este navegador não suporta notificações",
|
||||
"lastfmLinkSuccess": "Sua conta no Last.fm foi conectada com sucesso",
|
||||
|
@ -417,7 +417,9 @@
|
|||
"shareSuccess": "Link copiado para o clipboard : %{url}",
|
||||
"shareFailure": "Erro ao copiar o link %{url} para o clipboard",
|
||||
"downloadDialogTitle": "Baixar %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "Copie para o clipboard: Ctrl+C, Enter"
|
||||
"shareCopyToClipboard": "Copie para o clipboard: Ctrl+C, Enter",
|
||||
"remove_missing_title": "Remover arquivos ausentes",
|
||||
"remove_missing_content": "Você tem certeza que deseja remover os arquivos selecionados do banco de dados? Isso removerá permanentemente qualquer referência a eles, incluindo suas contagens de reprodução e classificações."
|
||||
},
|
||||
"menu": {
|
||||
"library": "Biblioteca",
|
||||
|
@ -431,7 +433,6 @@
|
|||
"language": "Língua",
|
||||
"defaultView": "Tela inicial",
|
||||
"desktop_notifications": "Notificações",
|
||||
"lastfmNotConfigured": "A API-Key do Last.fm não está configurada",
|
||||
"lastfmScrobbling": "Enviar scrobbles para Last.fm",
|
||||
"listenBrainzScrobbling": "Enviar scrobbles para ListenBrainz",
|
||||
"replaygain": "Modo ReplayGain",
|
||||
|
@ -440,7 +441,8 @@
|
|||
"none": "Desligado",
|
||||
"album": "Usar ganho do álbum",
|
||||
"track": "Usar ganho do faixa"
|
||||
}
|
||||
},
|
||||
"lastfmNotConfigured": "A API-Key do Last.fm não está configurada"
|
||||
}
|
||||
},
|
||||
"albumList": "Álbuns",
|
||||
|
|
|
@ -26,7 +26,13 @@
|
|||
"bpm": "BPM",
|
||||
"playDate": "Последнее воспроизведение",
|
||||
"channels": "Каналы",
|
||||
"createdAt": "Дата добавления"
|
||||
"createdAt": "Дата добавления",
|
||||
"grouping": "",
|
||||
"mood": "",
|
||||
"participants": "",
|
||||
"tags": "",
|
||||
"mappedTags": "",
|
||||
"rawTags": ""
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "В очередь",
|
||||
|
@ -58,7 +64,13 @@
|
|||
"originalDate": "Оригинал",
|
||||
"releaseDate": "Релиз",
|
||||
"releases": "Релиз |||| Релиза |||| Релизов",
|
||||
"released": "Релиз"
|
||||
"released": "Релиз",
|
||||
"recordLabel": "",
|
||||
"catalogNum": "",
|
||||
"releaseType": "",
|
||||
"grouping": "",
|
||||
"media": "",
|
||||
"mood": ""
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "Играть",
|
||||
|
@ -89,7 +101,23 @@
|
|||
"playCount": "Проигран",
|
||||
"rating": "Рейтинг",
|
||||
"genre": "Жанр",
|
||||
"size": "Размер"
|
||||
"size": "Размер",
|
||||
"role": ""
|
||||
},
|
||||
"roles": {
|
||||
"albumartist": "",
|
||||
"artist": "",
|
||||
"composer": "",
|
||||
"conductor": "",
|
||||
"lyricist": "",
|
||||
"arranger": "",
|
||||
"producer": "",
|
||||
"director": "",
|
||||
"engineer": "",
|
||||
"mixer": "",
|
||||
"remixer": "",
|
||||
"djmixer": "",
|
||||
"performer": ""
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
|
@ -135,11 +163,11 @@
|
|||
}
|
||||
},
|
||||
"transcoding": {
|
||||
"name": "Транскодирование |||| Транскодирование",
|
||||
"name": "Транскодирование |||| Транскодирование",
|
||||
"fields": {
|
||||
"name": "Название",
|
||||
"targetFormat": "Целевой формат",
|
||||
"defaultBitRate": "Стандартный битрейт",
|
||||
"defaultBitRate": "Битрейт по умолчанию",
|
||||
"command": "Команда"
|
||||
}
|
||||
},
|
||||
|
@ -183,9 +211,9 @@
|
|||
}
|
||||
},
|
||||
"share": {
|
||||
"name": "Ссылка доступа |||| Ссылки доступа",
|
||||
"name": "Общий доступ |||| Общий доступ",
|
||||
"fields": {
|
||||
"username": "Кто поделился",
|
||||
"username": "Поделился",
|
||||
"url": "Ссылка",
|
||||
"description": "Описание",
|
||||
"contents": "Содержание",
|
||||
|
@ -194,9 +222,23 @@
|
|||
"visitCount": "Посещения",
|
||||
"format": "Формат",
|
||||
"maxBitRate": "Макс. Битрейт",
|
||||
"updatedAt": "Обновлено",
|
||||
"updatedAt": "Обновлено в",
|
||||
"createdAt": "Создано",
|
||||
"downloadable": "Разрешить скачивание?"
|
||||
"downloadable": "Разрешить загрузку?"
|
||||
}
|
||||
},
|
||||
"missing": {
|
||||
"name": "",
|
||||
"fields": {
|
||||
"path": "",
|
||||
"size": "",
|
||||
"updatedAt": ""
|
||||
},
|
||||
"actions": {
|
||||
"remove": ""
|
||||
},
|
||||
"notifications": {
|
||||
"removed": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -213,7 +255,7 @@
|
|||
"sign_in": "Войти",
|
||||
"sign_in_error": "Ошибка аутентификации, попробуйте снова",
|
||||
"logout": "Выйти",
|
||||
"insightsCollectionNote": "Navidrome собирает анонимные данные об использовании для\nулучшения проекта. Нажмите [здесь], чтобы\nузнать больше или отказаться"
|
||||
"insightsCollectionNote": ""
|
||||
},
|
||||
"validation": {
|
||||
"invalidChars": "Пожалуйста, используйте только буквы и цифры",
|
||||
|
@ -375,7 +417,9 @@
|
|||
"shareSuccess": "URL скопирован в буфер обмена: %{url}",
|
||||
"shareFailure": "Ошибка копирования URL-адреса %{url} в буфер обмена",
|
||||
"downloadDialogTitle": "Скачать %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "Копировать в буфер обмена: Ctrl+C, Enter"
|
||||
"shareCopyToClipboard": "Копировать в буфер обмена: Ctrl+C, Enter",
|
||||
"remove_missing_title": "",
|
||||
"remove_missing_content": ""
|
||||
},
|
||||
"menu": {
|
||||
"library": "Библиотека",
|
||||
|
@ -389,7 +433,6 @@
|
|||
"language": "Язык",
|
||||
"defaultView": "Вид по умолчанию",
|
||||
"desktop_notifications": "Уведомления на рабочем столе",
|
||||
"lastfmNotConfigured": "API-ключ Last.fm не настроен",
|
||||
"lastfmScrobbling": "Скробблинг Last.fm",
|
||||
"listenBrainzScrobbling": "Скробблинг ListenBrainz",
|
||||
"replaygain": "ReplayGain режим",
|
||||
|
@ -398,7 +441,8 @@
|
|||
"none": "Отключить",
|
||||
"album": "Использовать усиление альбома",
|
||||
"track": "Использовать усиление трека"
|
||||
}
|
||||
},
|
||||
"lastfmNotConfigured": ""
|
||||
}
|
||||
},
|
||||
"albumList": "Альбомы",
|
||||
|
@ -435,12 +479,12 @@
|
|||
"links": {
|
||||
"homepage": "Главная",
|
||||
"source": "Код",
|
||||
"featureRequests": "Предложения"
|
||||
},
|
||||
"lastInsightsCollection": "Последний сбор данных",
|
||||
"insights": {
|
||||
"disabled": "Отключено",
|
||||
"waiting": "Пока нет"
|
||||
"featureRequests": "Предложения",
|
||||
"lastInsightsCollection": "",
|
||||
"insights": {
|
||||
"disabled": "",
|
||||
"waiting": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
|
@ -465,4 +509,4 @@
|
|||
"current_song": "Перейти к текущей песне"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,7 +26,13 @@
|
|||
"bpm": "BPM",
|
||||
"playDate": "Son Oynatılma",
|
||||
"channels": "Kanal",
|
||||
"createdAt": "Eklenme tarihi"
|
||||
"createdAt": "Eklenme tarihi",
|
||||
"grouping": "Gruplama",
|
||||
"mood": "Mod",
|
||||
"participants": "Ek katılımcılar",
|
||||
"tags": "Ek Etiketler",
|
||||
"mappedTags": "Eşlenen etiketler",
|
||||
"rawTags": "Ham etiketler"
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "Oynatma Sırasına Ekle",
|
||||
|
@ -58,7 +64,13 @@
|
|||
"originalDate": "Orijinal",
|
||||
"releaseDate": "Yayınlanma Tarihi",
|
||||
"releases": "Yayınlanan |||| Yayınlananlar",
|
||||
"released": "Yayınlandı"
|
||||
"released": "Yayınlandı",
|
||||
"recordLabel": "Etiket",
|
||||
"catalogNum": "Katalog Numarası",
|
||||
"releaseType": "Tür",
|
||||
"grouping": "Gruplama",
|
||||
"media": "Medya",
|
||||
"mood": "Mod"
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "Oynat",
|
||||
|
@ -89,7 +101,23 @@
|
|||
"playCount": "Oynatmalar",
|
||||
"rating": "Derecelendirme",
|
||||
"genre": "Tür",
|
||||
"size": "Boyut"
|
||||
"size": "Boyut",
|
||||
"role": "Rol"
|
||||
},
|
||||
"roles": {
|
||||
"albumartist": "Albüm Sanatçısı |||| Albüm Sanatçısı",
|
||||
"artist": "Sanatçı |||| Sanatçı",
|
||||
"composer": "Besteci |||| Besteci",
|
||||
"conductor": "Şef |||| Şef",
|
||||
"lyricist": "Söz Yazarı |||| Söz Yazarı",
|
||||
"arranger": "Düzenleyici |||| Düzenleyici",
|
||||
"producer": "Yapımcı |||| Yapımcı",
|
||||
"director": "Yönetmen |||| Yönetmen",
|
||||
"engineer": "Teknisyen |||| Teknisyen",
|
||||
"mixer": "Mikser |||| Mikser",
|
||||
"remixer": "Remiks |||| Remiks",
|
||||
"djmixer": "DJ Mikseri |||| DJ Mikseri",
|
||||
"performer": "Sanatçı |||| Sanatçı"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
|
@ -198,6 +226,20 @@
|
|||
"createdAt": "Oluşturma Tarihi",
|
||||
"downloadable": "İndirmelere İzin Ver"
|
||||
}
|
||||
},
|
||||
"missing": {
|
||||
"name": "Eksik Dosya |||| Eksik Dosyalar",
|
||||
"fields": {
|
||||
"path": "Yol",
|
||||
"size": "Boyut",
|
||||
"updatedAt": "Kaybolma"
|
||||
},
|
||||
"actions": {
|
||||
"remove": "Kaldır"
|
||||
},
|
||||
"notifications": {
|
||||
"removed": "Eksik dosya(lar) kaldırıldı"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ra": {
|
||||
|
@ -275,7 +317,7 @@
|
|||
"loading": "Yükleniyor",
|
||||
"not_found": "Bulunamadı",
|
||||
"show": "%{name} #%{id}",
|
||||
"empty": "Henüz %{name} Oluşturulmadı.",
|
||||
"empty": "%{name} henüz yok.",
|
||||
"invite": "Bir tane oluşturmak ister misin?"
|
||||
},
|
||||
"input": {
|
||||
|
@ -375,7 +417,9 @@
|
|||
"shareSuccess": "URL panoya kopyalandı: %{url}",
|
||||
"shareFailure": "%{url} panoya kopyalanırken hata oluştu",
|
||||
"downloadDialogTitle": "%{resource}: '%{name}' (%{size}) dosyasını indirin",
|
||||
"shareCopyToClipboard": "Panoya kopyala: Ctrl+C, Enter"
|
||||
"shareCopyToClipboard": "Panoya kopyala: Ctrl+C, Enter",
|
||||
"remove_missing_title": "Eksik dosyaları kaldır",
|
||||
"remove_missing_content": "Seçili eksik dosyaları veritabanından kaldırmak istediğinizden emin misiniz? Bu, oynatma sayıları ve derecelendirmeleri dahil olmak üzere bunlara ilişkin tüm referansları kalıcı olarak kaldıracaktır."
|
||||
},
|
||||
"menu": {
|
||||
"library": "Kütüphane",
|
||||
|
|
|
@ -390,7 +390,7 @@ func (p *phaseFolders) persistChanges(entry *folderEntry) (*folderEntry, error)
|
|||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}, "scanner: persist changes")
|
||||
if err != nil {
|
||||
log.Error(p.ctx, "Scanner: Error persisting changes to DB", "folder", entry.path, err)
|
||||
}
|
||||
|
@ -464,7 +464,7 @@ func (p *phaseFolders) finalize(err error) error {
|
|||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}, "scanner: finalize phaseFolders")
|
||||
return errors.Join(err, errF)
|
||||
}
|
||||
|
||||
|
|
|
@ -106,79 +106,75 @@ func (p *phaseMissingTracks) stages() []ppl.Stage[*missingTracks] {
|
|||
}
|
||||
|
||||
func (p *phaseMissingTracks) processMissingTracks(in *missingTracks) (*missingTracks, error) {
|
||||
err := p.ds.WithTx(func(tx model.DataStore) error {
|
||||
for _, ms := range in.missing {
|
||||
var exactMatch model.MediaFile
|
||||
var equivalentMatch model.MediaFile
|
||||
for _, ms := range in.missing {
|
||||
var exactMatch model.MediaFile
|
||||
var equivalentMatch model.MediaFile
|
||||
|
||||
// Identify exact and equivalent matches
|
||||
for _, mt := range in.matched {
|
||||
if ms.Equals(mt) {
|
||||
exactMatch = mt
|
||||
break // Prioritize exact match
|
||||
}
|
||||
if ms.IsEquivalent(mt) {
|
||||
equivalentMatch = mt
|
||||
}
|
||||
// Identify exact and equivalent matches
|
||||
for _, mt := range in.matched {
|
||||
if ms.Equals(mt) {
|
||||
exactMatch = mt
|
||||
break // Prioritize exact match
|
||||
}
|
||||
|
||||
// Use the exact match if found
|
||||
if exactMatch.ID != "" {
|
||||
log.Debug(p.ctx, "Scanner: Found missing track in a new place", "missing", ms.Path, "movedTo", exactMatch.Path, "lib", in.lib.Name)
|
||||
err := p.moveMatched(tx, exactMatch, ms)
|
||||
if err != nil {
|
||||
log.Error(p.ctx, "Scanner: Error moving matched track", "missing", ms.Path, "movedTo", exactMatch.Path, "lib", in.lib.Name, err)
|
||||
return err
|
||||
}
|
||||
p.totalMatched.Add(1)
|
||||
continue
|
||||
}
|
||||
|
||||
// If there is only one missing and one matched track, consider them equivalent (same PID)
|
||||
if len(in.missing) == 1 && len(in.matched) == 1 {
|
||||
singleMatch := in.matched[0]
|
||||
log.Debug(p.ctx, "Scanner: Found track with same persistent ID in a new place", "missing", ms.Path, "movedTo", singleMatch.Path, "lib", in.lib.Name)
|
||||
err := p.moveMatched(tx, singleMatch, ms)
|
||||
if err != nil {
|
||||
log.Error(p.ctx, "Scanner: Error updating matched track", "missing", ms.Path, "movedTo", singleMatch.Path, "lib", in.lib.Name, err)
|
||||
return err
|
||||
}
|
||||
p.totalMatched.Add(1)
|
||||
continue
|
||||
}
|
||||
|
||||
// Use the equivalent match if no other better match was found
|
||||
if equivalentMatch.ID != "" {
|
||||
log.Debug(p.ctx, "Scanner: Found missing track with same base path", "missing", ms.Path, "movedTo", equivalentMatch.Path, "lib", in.lib.Name)
|
||||
err := p.moveMatched(tx, equivalentMatch, ms)
|
||||
if err != nil {
|
||||
log.Error(p.ctx, "Scanner: Error updating matched track", "missing", ms.Path, "movedTo", equivalentMatch.Path, "lib", in.lib.Name, err)
|
||||
return err
|
||||
}
|
||||
p.totalMatched.Add(1)
|
||||
if ms.IsEquivalent(mt) {
|
||||
equivalentMatch = mt
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
// Use the exact match if found
|
||||
if exactMatch.ID != "" {
|
||||
log.Debug(p.ctx, "Scanner: Found missing track in a new place", "missing", ms.Path, "movedTo", exactMatch.Path, "lib", in.lib.Name)
|
||||
err := p.moveMatched(exactMatch, ms)
|
||||
if err != nil {
|
||||
log.Error(p.ctx, "Scanner: Error moving matched track", "missing", ms.Path, "movedTo", exactMatch.Path, "lib", in.lib.Name, err)
|
||||
return nil, err
|
||||
}
|
||||
p.totalMatched.Add(1)
|
||||
continue
|
||||
}
|
||||
|
||||
// If there is only one missing and one matched track, consider them equivalent (same PID)
|
||||
if len(in.missing) == 1 && len(in.matched) == 1 {
|
||||
singleMatch := in.matched[0]
|
||||
log.Debug(p.ctx, "Scanner: Found track with same persistent ID in a new place", "missing", ms.Path, "movedTo", singleMatch.Path, "lib", in.lib.Name)
|
||||
err := p.moveMatched(singleMatch, ms)
|
||||
if err != nil {
|
||||
log.Error(p.ctx, "Scanner: Error updating matched track", "missing", ms.Path, "movedTo", singleMatch.Path, "lib", in.lib.Name, err)
|
||||
return nil, err
|
||||
}
|
||||
p.totalMatched.Add(1)
|
||||
continue
|
||||
}
|
||||
|
||||
// Use the equivalent match if no other better match was found
|
||||
if equivalentMatch.ID != "" {
|
||||
log.Debug(p.ctx, "Scanner: Found missing track with same base path", "missing", ms.Path, "movedTo", equivalentMatch.Path, "lib", in.lib.Name)
|
||||
err := p.moveMatched(equivalentMatch, ms)
|
||||
if err != nil {
|
||||
log.Error(p.ctx, "Scanner: Error updating matched track", "missing", ms.Path, "movedTo", equivalentMatch.Path, "lib", in.lib.Name, err)
|
||||
return nil, err
|
||||
}
|
||||
p.totalMatched.Add(1)
|
||||
}
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func (p *phaseMissingTracks) moveMatched(tx model.DataStore, mt, ms model.MediaFile) error {
|
||||
discardedID := mt.ID
|
||||
mt.ID = ms.ID
|
||||
err := tx.MediaFile(p.ctx).Put(&mt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update matched track: %w", err)
|
||||
}
|
||||
err = tx.MediaFile(p.ctx).Delete(discardedID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete discarded track: %w", err)
|
||||
}
|
||||
p.state.changesDetected.Store(true)
|
||||
return nil
|
||||
func (p *phaseMissingTracks) moveMatched(mt, ms model.MediaFile) error {
|
||||
return p.ds.WithTx(func(tx model.DataStore) error {
|
||||
discardedID := mt.ID
|
||||
mt.ID = ms.ID
|
||||
err := tx.MediaFile(p.ctx).Put(&mt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update matched track: %w", err)
|
||||
}
|
||||
err = tx.MediaFile(p.ctx).Delete(discardedID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete discarded track: %w", err)
|
||||
}
|
||||
p.state.changesDetected.Store(true)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p *phaseMissingTracks) finalize(err error) error {
|
||||
|
|
|
@ -104,19 +104,13 @@ func (p *phaseRefreshAlbums) refreshAlbum(album *model.Album) (*model.Album, err
|
|||
return nil, nil
|
||||
}
|
||||
start := time.Now()
|
||||
err := p.ds.WithTx(func(tx model.DataStore) error {
|
||||
err := tx.Album(p.ctx).Put(album)
|
||||
log.Debug(p.ctx, "Scanner: refreshing album", "album_id", album.ID, "name", album.Name, "songCount", album.SongCount, "elapsed", time.Since(start))
|
||||
if err != nil {
|
||||
return fmt.Errorf("refreshing album %s: %w", album.ID, err)
|
||||
}
|
||||
p.refreshed.Add(1)
|
||||
p.state.changesDetected.Store(true)
|
||||
return nil
|
||||
})
|
||||
err := p.ds.Album(p.ctx).Put(album)
|
||||
log.Debug(p.ctx, "Scanner: refreshing album", "album_id", album.ID, "name", album.Name, "songCount", album.SongCount, "elapsed", time.Since(start), err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("refreshing album %s: %w", album.ID, err)
|
||||
}
|
||||
p.refreshed.Add(1)
|
||||
p.state.changesDetected.Store(true)
|
||||
return album, nil
|
||||
}
|
||||
|
||||
|
@ -135,23 +129,21 @@ func (p *phaseRefreshAlbums) finalize(err error) error {
|
|||
log.Debug(p.ctx, "Scanner: No changes detected, skipping refreshing annotations")
|
||||
return nil
|
||||
}
|
||||
return p.ds.WithTx(func(tx model.DataStore) error {
|
||||
// Refresh album annotations
|
||||
start := time.Now()
|
||||
cnt, err := tx.Album(p.ctx).RefreshPlayCounts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("refreshing album annotations: %w", err)
|
||||
}
|
||||
log.Debug(p.ctx, "Scanner: Refreshed album annotations", "albums", cnt, "elapsed", time.Since(start))
|
||||
// Refresh album annotations
|
||||
start := time.Now()
|
||||
cnt, err := p.ds.Album(p.ctx).RefreshPlayCounts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("refreshing album annotations: %w", err)
|
||||
}
|
||||
log.Debug(p.ctx, "Scanner: Refreshed album annotations", "albums", cnt, "elapsed", time.Since(start))
|
||||
|
||||
// Refresh artist annotations
|
||||
start = time.Now()
|
||||
cnt, err = tx.Artist(p.ctx).RefreshPlayCounts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("refreshing artist annotations: %w", err)
|
||||
}
|
||||
log.Debug(p.ctx, "Scanner: Refreshed artist annotations", "artists", cnt, "elapsed", time.Since(start))
|
||||
p.state.changesDetected.Store(true)
|
||||
return nil
|
||||
})
|
||||
// Refresh artist annotations
|
||||
start = time.Now()
|
||||
cnt, err = p.ds.Artist(p.ctx).RefreshPlayCounts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("refreshing artist annotations: %w", err)
|
||||
}
|
||||
log.Debug(p.ctx, "Scanner: Refreshed artist annotations", "artists", cnt, "elapsed", time.Since(start))
|
||||
p.state.changesDetected.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ func (s *scannerImpl) runGC(ctx context.Context, state *scanState) func() error
|
|||
log.Debug(ctx, "Scanner: No changes detected, skipping GC")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}, "scanner: GC")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,24 +138,22 @@ func (s *scannerImpl) runRefreshStats(ctx context.Context, state *scanState) fun
|
|||
log.Debug(ctx, "Scanner: No changes detected, skipping refreshing stats")
|
||||
return nil
|
||||
}
|
||||
return s.ds.WithTx(func(tx model.DataStore) error {
|
||||
start := time.Now()
|
||||
stats, err := tx.Artist(ctx).RefreshStats()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error refreshing artists stats", err)
|
||||
return fmt.Errorf("refreshing artists stats: %w", err)
|
||||
}
|
||||
log.Debug(ctx, "Scanner: Refreshed artist stats", "stats", stats, "elapsed", time.Since(start))
|
||||
start := time.Now()
|
||||
stats, err := s.ds.Artist(ctx).RefreshStats()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error refreshing artists stats", err)
|
||||
return fmt.Errorf("refreshing artists stats: %w", err)
|
||||
}
|
||||
log.Debug(ctx, "Scanner: Refreshed artist stats", "stats", stats, "elapsed", time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
err = tx.Tag(ctx).UpdateCounts()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error updating tag counts", err)
|
||||
return fmt.Errorf("updating tag counts: %w", err)
|
||||
}
|
||||
log.Debug(ctx, "Scanner: Updated tag counts", "elapsed", time.Since(start))
|
||||
return nil
|
||||
})
|
||||
start = time.Now()
|
||||
err = s.ds.Tag(ctx).UpdateCounts()
|
||||
if err != nil {
|
||||
log.Error(ctx, "Scanner: Error updating tag counts", err)
|
||||
return fmt.Errorf("updating tag counts: %w", err)
|
||||
}
|
||||
log.Debug(ctx, "Scanner: Updated tag counts", "elapsed", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,7 +187,7 @@ func (s *scannerImpl) runUpdateLibraries(ctx context.Context, libs model.Librari
|
|||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}, "scanner: update libraries")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ func initialSetup(ds model.DataStore) {
|
|||
|
||||
err = properties.Put(consts.InitialSetupFlagKey, time.Now().String())
|
||||
return err
|
||||
})
|
||||
}, "initial setup")
|
||||
}
|
||||
|
||||
// If the Dev Admin user is not present, create it
|
||||
|
|
|
@ -100,7 +100,7 @@ func deleteFromPlaylist(ds model.DataStore) http.HandlerFunc {
|
|||
p := req.Params(r)
|
||||
playlistId, _ := p.String(":playlistId")
|
||||
ids, _ := p.Strings("id")
|
||||
err := ds.WithTx(func(tx model.DataStore) error {
|
||||
err := ds.WithTxImmediate(func(tx model.DataStore) error {
|
||||
tracksRepo := tx.Playlist(r.Context()).Tracks(playlistId, true)
|
||||
return tracksRepo.Delete(ids...)
|
||||
})
|
||||
|
|
|
@ -112,7 +112,7 @@ func (api *Router) setStar(ctx context.Context, star bool, ids ...string) error
|
|||
return nil
|
||||
}
|
||||
event := &events.RefreshResource{}
|
||||
err := api.ds.WithTx(func(tx model.DataStore) error {
|
||||
err := api.ds.WithTxImmediate(func(tx model.DataStore) error {
|
||||
for _, id := range ids {
|
||||
exist, err := tx.Album(ctx).Exists(id)
|
||||
if err != nil {
|
||||
|
|
|
@ -67,9 +67,6 @@ func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*respons
|
|||
square := p.BoolOr("square", false)
|
||||
|
||||
imgReader, lastUpdate, err := api.artwork.GetOrPlaceholder(ctx, id, size, square)
|
||||
w.Header().Set("cache-control", "public, max-age=315360000")
|
||||
w.Header().Set("last-modified", lastUpdate.Format(time.RFC1123))
|
||||
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
return nil, nil
|
||||
|
@ -82,6 +79,9 @@ func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*respons
|
|||
}
|
||||
|
||||
defer imgReader.Close()
|
||||
w.Header().Set("cache-control", "public, max-age=315360000")
|
||||
w.Header().Set("last-modified", lastUpdate.Format(time.RFC1123))
|
||||
|
||||
cnt, err := io.Copy(w, imgReader)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error sending image", "count", cnt, err)
|
||||
|
|
|
@ -58,7 +58,7 @@ func (api *Router) getPlaylist(ctx context.Context, id string) (*responses.Subso
|
|||
}
|
||||
|
||||
func (api *Router) create(ctx context.Context, playlistId, name string, ids []string) (string, error) {
|
||||
err := api.ds.WithTx(func(tx model.DataStore) error {
|
||||
err := api.ds.WithTxImmediate(func(tx model.DataStore) error {
|
||||
owner := getUser(ctx)
|
||||
var pls *model.Playlist
|
||||
var err error
|
||||
|
|
3
tests/fixtures/playlists/pls1.m3u
vendored
3
tests/fixtures/playlists/pls1.m3u
vendored
|
@ -1,3 +1,2 @@
|
|||
test.mp3
|
||||
test.ogg
|
||||
file:///tests/fixtures/01%20Invisible%20(RED)%20Edit%20Version.mp3
|
||||
test.ogg
|
6
tests/fixtures/playlists/subfolder2/pls2.m3u
vendored
6
tests/fixtures/playlists/subfolder2/pls2.m3u
vendored
|
@ -1,2 +1,4 @@
|
|||
test.mp3
|
||||
test.ogg
|
||||
../test.mp3
|
||||
../test.ogg
|
||||
/tests/fixtures/01%20Invisible%20(RED)%20Edit%20Version.mp3
|
||||
/invalid/path/xyz.mp3
|
|
@ -209,7 +209,11 @@ func (db *MockDataStore) Radio(ctx context.Context) model.RadioRepository {
|
|||
return db.MockedRadio
|
||||
}
|
||||
|
||||
func (db *MockDataStore) WithTx(block func(model.DataStore) error) error {
|
||||
func (db *MockDataStore) WithTx(block func(tx model.DataStore) error, label ...string) error {
|
||||
return block(db)
|
||||
}
|
||||
|
||||
func (db *MockDataStore) WithTxImmediate(block func(tx model.DataStore) error, label ...string) error {
|
||||
return block(db)
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,11 @@ func (m *MockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
|||
return nil, errors.New("error")
|
||||
}
|
||||
if d, ok := m.data[id]; ok {
|
||||
return d, nil
|
||||
// Intentionally clone the file and remove participants. This should
|
||||
// catch any caller that actually means to call GetWithParticipants
|
||||
res := *d
|
||||
res.Participants = model.Participants{}
|
||||
return &res, nil
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package tests
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
@ -18,7 +17,7 @@ func TempFileName(t testingT, prefix, suffix string) string {
|
|||
return filepath.Join(t.TempDir(), prefix+id.NewRandom()+suffix)
|
||||
}
|
||||
|
||||
func TempFile(t testingT, prefix, suffix string) (fs.File, string, error) {
|
||||
func TempFile(t testingT, prefix, suffix string) (*os.File, string, error) {
|
||||
name := TempFileName(t, prefix, suffix)
|
||||
f, err := os.Create(name)
|
||||
return f, name, err
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
User-agent: bingbot
|
||||
Disallow: /manifest.webmanifest
|
||||
|
||||
User-agent: *
|
||||
Disallow: /
|
|
@ -26,6 +26,9 @@ const useStyles = makeStyles({
|
|||
tableCell: {
|
||||
width: '17.5%',
|
||||
},
|
||||
value: {
|
||||
whiteSpace: 'pre-line',
|
||||
},
|
||||
})
|
||||
|
||||
const AlbumInfo = (props) => {
|
||||
|
@ -113,7 +116,9 @@ const AlbumInfo = (props) => {
|
|||
})}
|
||||
:
|
||||
</TableCell>
|
||||
<TableCell align="left">{data[key]}</TableCell>
|
||||
<TableCell align="left" className={classes.value}>
|
||||
{data[key]}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
|
|
|
@ -60,9 +60,16 @@ const AlbumShowLayout = (props) => {
|
|||
let perPage = 0
|
||||
let pagination = null
|
||||
|
||||
if (record?.stats?.['artist']?.albumCount > maxPerPage) {
|
||||
const count = Math.max(
|
||||
record?.stats?.['albumartist']?.albumCount || 0,
|
||||
record?.stats?.['artist']?.albumCount ?? 0,
|
||||
)
|
||||
|
||||
if (count > maxPerPage) {
|
||||
perPage = Math.trunc(maxPerPage / perPageOptions[0]) * perPageOptions[0]
|
||||
const rowsPerPageOptions = [1, 2, 3].map((option) => option * perPage)
|
||||
const rowsPerPageOptions = [1, 2, 3].map((option) =>
|
||||
Math.trunc(option * (perPage / 3)),
|
||||
)
|
||||
pagination = <Pagination rowsPerPageOptions={rowsPerPageOptions} />
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ const ALink = withWidth()((props) => {
|
|||
{...rest}
|
||||
>
|
||||
{artist.name}
|
||||
{artist.subroles?.length > 0 ? ` (${artist.subroles.join(', ')})` : ''}
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
|
@ -89,19 +90,29 @@ export const ArtistLinkField = ({ record, className, limit, source }) => {
|
|||
}
|
||||
|
||||
// Dedupe artists, only shows the first 3
|
||||
const seen = new Set()
|
||||
const seen = new Map()
|
||||
const dedupedArtists = []
|
||||
let limitedShow = false
|
||||
|
||||
for (const artist of artists ?? []) {
|
||||
if (!seen.has(artist.id)) {
|
||||
seen.add(artist.id)
|
||||
|
||||
if (dedupedArtists.length < limit) {
|
||||
dedupedArtists.push(artist)
|
||||
seen.set(artist.id, dedupedArtists.length)
|
||||
dedupedArtists.push({
|
||||
...artist,
|
||||
subroles: artist.subRole ? [artist.subRole] : [],
|
||||
})
|
||||
} else {
|
||||
limitedShow = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
const position = seen.get(artist.id)
|
||||
|
||||
if (position !== -1) {
|
||||
const existing = dedupedArtists[position]
|
||||
if (artist.subRole && !existing.subroles.includes(artist.subRole)) {
|
||||
existing.subroles.push(artist.subRole)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,9 @@ const useStyles = makeStyles({
|
|||
tableCell: {
|
||||
width: '17.5%',
|
||||
},
|
||||
value: {
|
||||
whiteSpace: 'pre-line',
|
||||
},
|
||||
})
|
||||
|
||||
export const SongInfo = (props) => {
|
||||
|
@ -111,27 +114,27 @@ export const SongInfo = (props) => {
|
|||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table aria-label="song details" size="small">
|
||||
<TableBody>
|
||||
{record.rawTags && (
|
||||
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
|
||||
<Tab
|
||||
label={translate(`resources.song.fields.mappedTags`)}
|
||||
id="mapped-tags-tab"
|
||||
aria-controls="mapped-tags-body"
|
||||
/>
|
||||
<Tab
|
||||
label={translate(`resources.song.fields.rawTags`)}
|
||||
id="raw-tags-tab"
|
||||
aria-controls="raw-tags-body"
|
||||
/>
|
||||
</Tabs>
|
||||
)}
|
||||
<div
|
||||
hidden={tab === 1}
|
||||
id="mapped-tags-body"
|
||||
aria-labelledby={record.rawTags ? 'mapped-tags-tab' : undefined}
|
||||
>
|
||||
{record.rawTags && (
|
||||
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
|
||||
<Tab
|
||||
label={translate(`resources.song.fields.mappedTags`)}
|
||||
id="mapped-tags-tab"
|
||||
aria-controls="mapped-tags-body"
|
||||
/>
|
||||
<Tab
|
||||
label={translate(`resources.song.fields.rawTags`)}
|
||||
id="raw-tags-tab"
|
||||
aria-controls="raw-tags-body"
|
||||
/>
|
||||
</Tabs>
|
||||
)}
|
||||
<div
|
||||
hidden={tab == 1}
|
||||
id="mapped-tags-body"
|
||||
aria-labelledby={record.rawTags ? 'mapped-tags-tab' : undefined}
|
||||
>
|
||||
<Table aria-label="song details" size="small">
|
||||
<TableBody>
|
||||
{Object.keys(data).map((key) => {
|
||||
return (
|
||||
<TableRow key={`${record.id}-${key}`}>
|
||||
|
@ -141,7 +144,9 @@ export const SongInfo = (props) => {
|
|||
})}
|
||||
:
|
||||
</TableCell>
|
||||
<TableCell align="left">{data[key]}</TableCell>
|
||||
<TableCell align="left" className={classes.value}>
|
||||
{data[key]}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
|
@ -152,7 +157,7 @@ export const SongInfo = (props) => {
|
|||
scope="row"
|
||||
className={classes.tableCell}
|
||||
></TableCell>
|
||||
<TableCell align="left">
|
||||
<TableCell align="left" className={classes.value}>
|
||||
<h4>{translate(`resources.song.fields.tags`)}</h4>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
@ -162,16 +167,22 @@ export const SongInfo = (props) => {
|
|||
<TableCell scope="row" className={classes.tableCell}>
|
||||
{name}:
|
||||
</TableCell>
|
||||
<TableCell align="left">{values.join(' • ')}</TableCell>
|
||||
<TableCell align="left" className={classes.value}>
|
||||
{values.join(' • ')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</div>
|
||||
{record.rawTags && (
|
||||
<div
|
||||
hidden={tab === 0}
|
||||
id="raw-tags-body"
|
||||
aria-labelledby="raw-tags-tab"
|
||||
>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{record.rawTags && (
|
||||
<div
|
||||
hidden={tab === 0}
|
||||
id="raw-tags-body"
|
||||
aria-labelledby="raw-tags-tab"
|
||||
>
|
||||
<Table size="small" aria-label="song raw tags">
|
||||
<TableBody>
|
||||
<TableRow key={`${record.id}-raw-path`}>
|
||||
<TableCell scope="row" className={classes.tableCell}>
|
||||
{translate(`resources.song.fields.path`)}:
|
||||
|
@ -183,13 +194,15 @@ export const SongInfo = (props) => {
|
|||
<TableCell scope="row" className={classes.tableCell}>
|
||||
{key}:
|
||||
</TableCell>
|
||||
<TableCell align="left">{value.join(' • ')}</TableCell>
|
||||
<TableCell align="left" className={classes.value}>
|
||||
{value.join(' • ')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Link from '@material-ui/core/Link'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
|
@ -16,6 +16,8 @@ import config from '../config'
|
|||
import { DialogTitle } from './DialogTitle'
|
||||
import { DialogContent } from './DialogContent'
|
||||
import { INSIGHTS_DOC_URL } from '../consts.js'
|
||||
import subsonic from '../subsonic/index.js'
|
||||
import { Typography } from '@material-ui/core'
|
||||
|
||||
const links = {
|
||||
homepage: 'navidrome.org',
|
||||
|
@ -29,7 +31,7 @@ const links = {
|
|||
|
||||
const LinkToVersion = ({ version }) => {
|
||||
if (version === 'dev') {
|
||||
return <TableCell align="left">{version}</TableCell>
|
||||
return <>{version}</>
|
||||
}
|
||||
|
||||
const parts = version.split(' ')
|
||||
|
@ -41,12 +43,46 @@ const LinkToVersion = ({ version }) => {
|
|||
}...${commitID}`
|
||||
: `https://github.com/navidrome/navidrome/releases/tag/v${parts[0]}`
|
||||
return (
|
||||
<TableCell align="left">
|
||||
<>
|
||||
<Link href={url} target="_blank" rel="noopener noreferrer">
|
||||
{parts[0]}
|
||||
</Link>
|
||||
{' (' + commitID + ')'}
|
||||
</TableCell>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ShowVersion = ({ uiVersion, serverVersion }) => {
|
||||
const translate = useTranslate()
|
||||
|
||||
const showRefresh = uiVersion !== serverVersion
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell align="right" component="th" scope="row">
|
||||
{translate('menu.version')}:
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
<LinkToVersion version={serverVersion} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{showRefresh && (
|
||||
<TableRow>
|
||||
<TableCell align="right" component="th" scope="row">
|
||||
UI {translate('menu.version')}:
|
||||
</TableCell>
|
||||
<TableCell align="left">
|
||||
<LinkToVersion version={uiVersion} />
|
||||
<Link onClick={() => window.location.reload()}>
|
||||
<Typography variant={'caption'}>
|
||||
{' ' + translate('ra.notification.new_version')}
|
||||
</Typography>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -54,6 +90,23 @@ const AboutDialog = ({ open, onClose }) => {
|
|||
const translate = useTranslate()
|
||||
const { permissions } = usePermissions()
|
||||
const { data, loading } = useGetOne('insights', 'insights_status')
|
||||
const [serverVersion, setServerVersion] = useState('')
|
||||
const uiVersion = config.version
|
||||
|
||||
useEffect(() => {
|
||||
subsonic
|
||||
.ping()
|
||||
.then((resp) => resp.json['subsonic-response'])
|
||||
.then((data) => {
|
||||
if (data.status === 'ok') {
|
||||
setServerVersion(data.serverVersion)
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('error pinging server', e)
|
||||
})
|
||||
}, [setServerVersion])
|
||||
|
||||
const lastRun = !loading && data?.lastRun
|
||||
let insightsStatus = 'N/A'
|
||||
|
@ -74,12 +127,10 @@ const AboutDialog = ({ open, onClose }) => {
|
|||
<TableContainer component={Paper}>
|
||||
<Table aria-label={translate('menu.about')} size="small">
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell align="right" component="th" scope="row">
|
||||
{translate('menu.version')}:
|
||||
</TableCell>
|
||||
<LinkToVersion version={config.version} />
|
||||
</TableRow>
|
||||
<ShowVersion
|
||||
uiVersion={uiVersion}
|
||||
serverVersion={serverVersion}
|
||||
/>
|
||||
{Object.keys(links).map((key) => {
|
||||
return (
|
||||
<TableRow key={key}>
|
||||
|
|
|
@ -4,12 +4,15 @@ import { LinkToVersion } from './AboutDialog'
|
|||
import TableBody from '@material-ui/core/TableBody'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import Table from '@material-ui/core/Table'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
|
||||
const Wrapper = ({ version }) => (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<LinkToVersion version={version} />
|
||||
<TableCell>
|
||||
<LinkToVersion version={version} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
|
@ -29,6 +29,8 @@ const url = (command, id, options) => {
|
|||
return `/rest/${command}?${params.toString()}`
|
||||
}
|
||||
|
||||
const ping = () => httpClient(url('ping'))
|
||||
|
||||
const scrobble = (id, time, submission = true) =>
|
||||
httpClient(
|
||||
url('scrobble', id, {
|
||||
|
@ -88,6 +90,7 @@ const streamUrl = (id, options) => {
|
|||
|
||||
export default {
|
||||
url,
|
||||
ping,
|
||||
scrobble,
|
||||
nowPlaying,
|
||||
download,
|
||||
|
|
|
@ -140,7 +140,7 @@ var _ = Describe("Slice Utils", func() {
|
|||
Expect(count).To(Equal(expected))
|
||||
},
|
||||
Entry("returns empty slice for an empty input", "tests/fixtures/empty.txt", 0),
|
||||
Entry("returns the lines of a file", "tests/fixtures/playlists/pls1.m3u", 3),
|
||||
Entry("returns the lines of a file", "tests/fixtures/playlists/pls1.m3u", 2),
|
||||
Entry("returns empty if file does not exist", "tests/fixtures/NON-EXISTENT", 0),
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue