mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Compare commits
7 Commits
52cdda90dc
...
950aafd58b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
950aafd58b | ||
|
|
64a9260174 | ||
|
|
6a7381aa5a | ||
|
|
e36fef8692 | ||
|
|
9913235542 | ||
|
|
a87b6a50a6 | ||
|
|
2b30ed1520 |
@ -346,7 +346,7 @@ func startPluginManager(ctx context.Context) func() error {
|
||||
// TODO: Implement some struct tags to map flags to viper
|
||||
func init() {
|
||||
cobra.OnInitialize(func() {
|
||||
conf.InitConfig(cfgFile)
|
||||
conf.InitConfig(cfgFile, true)
|
||||
})
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "configfile", "c", "", `config file (default "./navidrome.toml")`)
|
||||
|
||||
@ -617,7 +617,7 @@ func init() {
|
||||
setViperDefaults()
|
||||
}
|
||||
|
||||
func InitConfig(cfgFile string) {
|
||||
func InitConfig(cfgFile string, loadEnvVars bool) {
|
||||
codecRegistry := viper.NewCodecRegistry()
|
||||
_ = codecRegistry.RegisterCodec("ini", ini.Codec{
|
||||
LoadOptions: ini.LoadOptions{
|
||||
@ -638,10 +638,12 @@ func InitConfig(cfgFile string) {
|
||||
}
|
||||
|
||||
_ = viper.BindEnv("port")
|
||||
viper.SetEnvPrefix("ND")
|
||||
replacer := strings.NewReplacer(".", "_")
|
||||
viper.SetEnvKeyReplacer(replacer)
|
||||
viper.AutomaticEnv()
|
||||
if loadEnvVars {
|
||||
viper.SetEnvPrefix("ND")
|
||||
replacer := strings.NewReplacer(".", "_")
|
||||
viper.SetEnvKeyReplacer(replacer)
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if viper.ConfigFileUsed() != "" && err != nil {
|
||||
|
||||
@ -31,7 +31,7 @@ var _ = Describe("Configuration", func() {
|
||||
filename := filepath.Join("testdata", "cfg."+format)
|
||||
|
||||
// Initialize config with the test file
|
||||
conf.InitConfig(filename)
|
||||
conf.InitConfig(filename, false)
|
||||
// Load the configuration (with noConfigDump=true)
|
||||
conf.Load(true)
|
||||
|
||||
|
||||
@ -113,9 +113,9 @@ func WithAdminUser(ctx context.Context, ds model.DataStore) context.Context {
|
||||
if err != nil {
|
||||
c, err := ds.User(ctx).CountAll()
|
||||
if c == 0 && err == nil {
|
||||
log.Debug(ctx, "Scanner: No admin user yet!", err)
|
||||
log.Debug(ctx, "No admin user yet!", err)
|
||||
} else {
|
||||
log.Error(ctx, "Scanner: No admin user found!", err)
|
||||
log.Error(ctx, "No admin user found!", err)
|
||||
}
|
||||
u = &model.User{}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import (
|
||||
"github.com/navidrome/navidrome/core/metrics/insights"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/plugins/schema"
|
||||
"github.com/navidrome/navidrome/utils/singleton"
|
||||
)
|
||||
@ -64,9 +65,16 @@ func GetInstance(ds model.DataStore, pluginLoader PluginLoader) Insights {
|
||||
}
|
||||
|
||||
func (c *insightsCollector) Run(ctx context.Context) {
|
||||
ctx = auth.WithAdminUser(ctx, c.ds)
|
||||
for {
|
||||
c.sendInsights(ctx)
|
||||
// Refresh admin context on each iteration to handle cases where
|
||||
// admin user wasn't available on previous runs
|
||||
insightsCtx := auth.WithAdminUser(ctx, c.ds)
|
||||
u, _ := request.UserFrom(insightsCtx)
|
||||
if !u.IsAdmin {
|
||||
log.Trace(insightsCtx, "No admin user available, skipping insights collection")
|
||||
} else {
|
||||
c.sendInsights(insightsCtx)
|
||||
}
|
||||
select {
|
||||
case <-time.After(consts.InsightsUpdateInterval):
|
||||
continue
|
||||
|
||||
@ -372,15 +372,18 @@ var _ = Describe("PlaylistRepository", func() {
|
||||
var testPlaylistID string
|
||||
var lib2ID int
|
||||
var restrictedUserID string
|
||||
var uniqueLibPath string
|
||||
|
||||
BeforeEach(func() {
|
||||
db := GetDBXBuilder()
|
||||
|
||||
// Generate unique IDs for this test run
|
||||
restrictedUserID = "restricted-user-" + time.Now().Format("20060102150405.000")
|
||||
uniqueSuffix := time.Now().Format("20060102150405.000")
|
||||
restrictedUserID = "restricted-user-" + uniqueSuffix
|
||||
uniqueLibPath = "/music/lib2-" + uniqueSuffix
|
||||
|
||||
// Create a second library
|
||||
_, err := db.DB().Exec("INSERT INTO library (name, path, created_at, updated_at) VALUES ('Library 2', '/music/lib2', datetime('now'), datetime('now'))")
|
||||
// Create a second library with unique name and path to avoid conflicts with other tests
|
||||
_, err := db.DB().Exec("INSERT INTO library (name, path, created_at, updated_at) VALUES (?, ?, datetime('now'), datetime('now'))", "Library 2-"+uniqueSuffix, uniqueLibPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
err = db.DB().QueryRow("SELECT last_insert_rowid()").Scan(&lib2ID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@ -420,7 +423,7 @@ var _ = Describe("PlaylistRepository", func() {
|
||||
ArtistID: "1",
|
||||
Album: "Test Album",
|
||||
AlbumID: "101",
|
||||
Path: "/music/lib2/song.mp3",
|
||||
Path: uniqueLibPath + "/song.mp3",
|
||||
LibraryID: lib2ID,
|
||||
Participants: model.Participants{},
|
||||
Tags: model.Tags{},
|
||||
|
||||
@ -2,6 +2,7 @@ package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
@ -45,6 +46,9 @@ var _ = Describe("Tag Library Filtering", func() {
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
|
||||
// Generate unique path suffix to avoid conflicts with other tests
|
||||
uniqueSuffix := time.Now().Format("20060102150405.000")
|
||||
|
||||
// Clean up database
|
||||
db := GetDBXBuilder()
|
||||
_, err := db.NewQuery("DELETE FROM library_tag").Execute()
|
||||
@ -57,12 +61,12 @@ var _ = Describe("Tag Library Filtering", func() {
|
||||
_, err = db.NewQuery("DELETE FROM library WHERE id > 1").Execute()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Create test libraries
|
||||
// Create test libraries with unique names and paths to avoid conflicts with other tests
|
||||
_, err = db.NewQuery("INSERT INTO library (id, name, path) VALUES ({:id}, {:name}, {:path})").
|
||||
Bind(dbx.Params{"id": libraryID2, "name": "Library 2", "path": "/music/lib2"}).Execute()
|
||||
Bind(dbx.Params{"id": libraryID2, "name": "Library 2-" + uniqueSuffix, "path": "/music/lib2-" + uniqueSuffix}).Execute()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_, err = db.NewQuery("INSERT INTO library (id, name, path) VALUES ({:id}, {:name}, {:path})").
|
||||
Bind(dbx.Params{"id": libraryID3, "name": "Library 3", "path": "/music/lib3"}).Execute()
|
||||
Bind(dbx.Params{"id": libraryID3, "name": "Library 3-" + uniqueSuffix, "path": "/music/lib3-" + uniqueSuffix}).Execute()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Give admin access to all libraries
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
"actions": {
|
||||
"playAll": "Afspil",
|
||||
"playNext": "Afspil næste",
|
||||
"addToQueue": "Afspil senere",
|
||||
"addToQueue": "Føj til kø",
|
||||
"shuffle": "Bland",
|
||||
"addToPlaylist": "Føj til afspilningsliste",
|
||||
"download": "Download",
|
||||
@ -301,14 +301,19 @@
|
||||
"actions": {
|
||||
"scan": "Scanningsbibliotek",
|
||||
"manageUsers": "Administrer brugeradgang",
|
||||
"viewDetails": "Se detaljer"
|
||||
"viewDetails": "Se detaljer",
|
||||
"quickScan": "hurtig skanning",
|
||||
"fullScan": "Fuld skanning"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Bibliotek oprettet",
|
||||
"updated": "Biblioteket er blevet opdateret",
|
||||
"deleted": "Biblioteket er blevet slettet",
|
||||
"scanStarted": "Biblioteksscanning startet",
|
||||
"scanCompleted": "Biblioteksscanning fuldført"
|
||||
"scanCompleted": "Biblioteksscanning fuldført",
|
||||
"quickScanStarted": "hurtig skanning startet",
|
||||
"fullScanStarted": "Fuld skanning startet",
|
||||
"scanError": "Kan ikke starte skanning. Tjek loggen"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Biblioteksnavn er påkrævet",
|
||||
@ -549,7 +554,7 @@
|
||||
"closeText": "Luk",
|
||||
"notContentText": "Ingen musik",
|
||||
"clickToPlayText": "Tryk for at afspille",
|
||||
"clickToPauseText": "Tryk for at pause",
|
||||
"clickToPauseText": "Tryk for at sætte på pause",
|
||||
"nextTrackText": "Næste nummer",
|
||||
"previousTrackText": "Forrige nummer",
|
||||
"reloadText": "Genindlæs",
|
||||
@ -604,7 +609,8 @@
|
||||
"serverDown": "OFFLINE",
|
||||
"scanType": "Type",
|
||||
"status": "Scanningsfejl",
|
||||
"elapsedTime": "Medgået tid"
|
||||
"elapsedTime": "Medgået tid",
|
||||
"selectiveScan": "Selektiv"
|
||||
},
|
||||
"help": {
|
||||
"title": "Navidrome genvejstaster",
|
||||
|
||||
@ -301,14 +301,19 @@
|
||||
"actions": {
|
||||
"scan": "Bibliothek scannen",
|
||||
"manageUsers": "Zugriff verwalten",
|
||||
"viewDetails": "Details ansehen"
|
||||
"viewDetails": "Details ansehen",
|
||||
"quickScan": "Schneller Scan",
|
||||
"fullScan": "Kompletter Scan"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Bibliothek erfolgreich erstellt",
|
||||
"updated": "Bibliothek erfolgreich geändert",
|
||||
"deleted": "Bibliothek erfolgreich gelöscht",
|
||||
"scanStarted": "Bibliothek Scan gestartet",
|
||||
"scanCompleted": "Bibliothek Scan vollständig"
|
||||
"scanCompleted": "Bibliothek Scan vollständig",
|
||||
"quickScanStarted": "Schneller Scan gestartet",
|
||||
"fullScanStarted": "Kompletter Scan gestartet",
|
||||
"scanError": "Fehler beim Starten des Scans. Logs prüfen"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Bibliotheksname ist Pflichtfeld",
|
||||
@ -604,7 +609,8 @@
|
||||
"serverDown": "OFFLINE",
|
||||
"scanType": "Typ",
|
||||
"status": "Scan Fehler",
|
||||
"elapsedTime": "Laufzeit"
|
||||
"elapsedTime": "Laufzeit",
|
||||
"selectiveScan": "Selektiver Scan"
|
||||
},
|
||||
"help": {
|
||||
"title": "Navidrome Hotkeys",
|
||||
|
||||
@ -301,14 +301,19 @@
|
||||
"actions": {
|
||||
"scan": "Σάρωση βιβλιοθήκης",
|
||||
"manageUsers": "Διαχείριση πρόσβασης χρήστη",
|
||||
"viewDetails": "Προβολή λεπτομερειών"
|
||||
"viewDetails": "Προβολή λεπτομερειών",
|
||||
"quickScan": "Γρήγορη σάρωση",
|
||||
"fullScan": "Πλήρης σάρωση"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Η βιβλιοθήκη δημιουργήθηκε με επιτυχία",
|
||||
"updated": "Η βιβλιοθήκη ενημερώθηκε με επιτυχία",
|
||||
"deleted": "Η βιβλιοθήκη διαγράφηκε με επιτυχία",
|
||||
"scanStarted": "Ξεκίνησε η σάρωση της βιβλιοθήκης",
|
||||
"scanCompleted": "Η σάρωση της βιβλιοθήκης ολοκληρώθηκε"
|
||||
"scanCompleted": "Η σάρωση της βιβλιοθήκης ολοκληρώθηκε",
|
||||
"quickScanStarted": "Η Γρήγορη Σάρωση ξεκίνησε",
|
||||
"fullScanStarted": "Η πλήρης σάρωση ξεκίνησε",
|
||||
"scanError": "Σφάλμα κατά την έναρξη της σάρωσης. Ελέγξτε τα αρχεία καταγραφής."
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Απαιτείται όνομα βιβλιοθήκης",
|
||||
@ -604,7 +609,8 @@
|
||||
"serverDown": "ΕΚΤΟΣ ΣΥΝΔΕΣΗΣ",
|
||||
"scanType": "Τύπος",
|
||||
"status": "Σφάλμα σάρωσης",
|
||||
"elapsedTime": "Χρόνος που πέρασε"
|
||||
"elapsedTime": "Χρόνος που πέρασε",
|
||||
"selectiveScan": "Εκλεκτικός"
|
||||
},
|
||||
"help": {
|
||||
"title": "Συντομεύσεις του Navidrome",
|
||||
|
||||
@ -27,12 +27,16 @@
|
||||
"playDate": "最後の再生",
|
||||
"channels": "チャンネル",
|
||||
"createdAt": "追加日",
|
||||
"grouping": "",
|
||||
"mood": "",
|
||||
"participants": "",
|
||||
"tags": "",
|
||||
"mappedTags": "",
|
||||
"rawTags": ""
|
||||
"grouping": "グループ分け",
|
||||
"mood": "ムード",
|
||||
"participants": "追加参加者",
|
||||
"tags": "追加タグ",
|
||||
"mappedTags": "マッピング済みタグ",
|
||||
"rawTags": "未処理タグ",
|
||||
"bitDepth": "ビット深度",
|
||||
"sampleRate": "サンプリングレート",
|
||||
"missing": "不明",
|
||||
"libraryName": "ライブラリ"
|
||||
},
|
||||
"actions": {
|
||||
"addToQueue": "最後に再生",
|
||||
@ -41,7 +45,8 @@
|
||||
"shuffleAll": "全曲シャッフル",
|
||||
"download": "ダウンロード",
|
||||
"playNext": "次に再生",
|
||||
"info": "詳細"
|
||||
"info": "詳細",
|
||||
"showInPlaylist": "含まれるプレイリスト"
|
||||
}
|
||||
},
|
||||
"album": {
|
||||
@ -65,12 +70,15 @@
|
||||
"releaseDate": "リリース日",
|
||||
"releases": "リリース",
|
||||
"released": "リリース",
|
||||
"recordLabel": "",
|
||||
"catalogNum": "",
|
||||
"releaseType": "",
|
||||
"grouping": "",
|
||||
"media": "",
|
||||
"mood": ""
|
||||
"recordLabel": "ラベル",
|
||||
"catalogNum": "カタログ番号",
|
||||
"releaseType": "タイプ",
|
||||
"grouping": "グループ分け",
|
||||
"media": "メディア",
|
||||
"mood": "ムード",
|
||||
"date": "録音日",
|
||||
"missing": "不明",
|
||||
"libraryName": "ライブラリ"
|
||||
},
|
||||
"actions": {
|
||||
"playAll": "再生",
|
||||
@ -102,22 +110,29 @@
|
||||
"rating": "レート",
|
||||
"genre": "ジャンル",
|
||||
"size": "サイズ",
|
||||
"role": ""
|
||||
"role": "役割",
|
||||
"missing": "不明"
|
||||
},
|
||||
"roles": {
|
||||
"albumartist": "",
|
||||
"artist": "",
|
||||
"composer": "",
|
||||
"conductor": "",
|
||||
"lyricist": "",
|
||||
"arranger": "",
|
||||
"producer": "",
|
||||
"director": "",
|
||||
"engineer": "",
|
||||
"mixer": "",
|
||||
"remixer": "",
|
||||
"djmixer": "",
|
||||
"performer": ""
|
||||
"albumartist": "アルバムアーティスト",
|
||||
"artist": "アーティスト",
|
||||
"composer": "作曲家",
|
||||
"conductor": "指揮者",
|
||||
"lyricist": "作詞家",
|
||||
"arranger": "編曲者",
|
||||
"producer": "プロデューサー",
|
||||
"director": "ディレクター",
|
||||
"engineer": "エンジニア",
|
||||
"mixer": "ミキサー",
|
||||
"remixer": "リミキサー",
|
||||
"djmixer": "DJ ミキサー",
|
||||
"performer": "演奏者",
|
||||
"maincredit": "アルバムアーティストもしくはアーティスト"
|
||||
},
|
||||
"actions": {
|
||||
"shuffle": "シャッフル",
|
||||
"radio": "ラジオ",
|
||||
"topSongs": "トップソング"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
@ -134,10 +149,12 @@
|
||||
"currentPassword": "現在のパスワード",
|
||||
"newPassword": "新しいパスワード",
|
||||
"token": "トークン",
|
||||
"lastAccessAt": "最終アクセス"
|
||||
"lastAccessAt": "最終アクセス",
|
||||
"libraries": "ライブラリ"
|
||||
},
|
||||
"helperTexts": {
|
||||
"name": "名前の変更は次回ログイン以降反映されます"
|
||||
"name": "名前の変更は次回ログイン以降反映されます",
|
||||
"libraries": "このユーザーに対して特定ライブラリを選択するか、デフォルトのライブラリを使用する場合は空欄のままにします"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "ユーザーが作成されました",
|
||||
@ -146,7 +163,12 @@
|
||||
},
|
||||
"message": {
|
||||
"listenBrainzToken": "ListenBrainzユーザートークンを入力",
|
||||
"clickHereForToken": "ここをクリックしトークンを入手"
|
||||
"clickHereForToken": "ここをクリックしトークンを入手",
|
||||
"selectAllLibraries": "全てのライブラリを選択",
|
||||
"adminAutoLibraries": "管理者ユーザーは自動的にすべてのライブラリにアクセスできます"
|
||||
},
|
||||
"validation": {
|
||||
"librariesRequired": "管理者以外のユーザーには少なくとも1つのライブラリを選択する必要があります"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
@ -190,11 +212,17 @@
|
||||
"addNewPlaylist": "'%{name}' を作成",
|
||||
"export": "エクスポート",
|
||||
"makePublic": "公開する",
|
||||
"makePrivate": "非公開にする"
|
||||
"makePrivate": "非公開にする",
|
||||
"saveQueue": "キューをプレイリストに保存",
|
||||
"searchOrCreate": "プレイリストを検索または入力して新規作成...",
|
||||
"pressEnterToCreate": "Enterキーを押して新しいプレイリストを作成",
|
||||
"removeFromSelection": "選択から削除"
|
||||
},
|
||||
"message": {
|
||||
"duplicate_song": "重複する曲を追加",
|
||||
"song_exist": "既にプレイリストに存在する曲です。追加しますか?"
|
||||
"song_exist": "既にプレイリストに存在する曲です。追加しますか?",
|
||||
"noPlaylistsFound": "プレイリストが見つかりません",
|
||||
"noPlaylists": "利用可能なプレイリストはありません"
|
||||
}
|
||||
},
|
||||
"radio": {
|
||||
@ -228,17 +256,77 @@
|
||||
}
|
||||
},
|
||||
"missing": {
|
||||
"name": "",
|
||||
"name": "欠落したファイル",
|
||||
"fields": {
|
||||
"path": "",
|
||||
"size": "",
|
||||
"updatedAt": ""
|
||||
"path": "パス",
|
||||
"size": "サイズ",
|
||||
"updatedAt": "欠落日",
|
||||
"libraryName": "ライブラリ"
|
||||
},
|
||||
"actions": {
|
||||
"remove": ""
|
||||
"remove": "削除",
|
||||
"remove_all": "全て削除"
|
||||
},
|
||||
"notifications": {
|
||||
"removed": ""
|
||||
"removed": "欠落ファイルが削除されました"
|
||||
},
|
||||
"empty": "ファイルの欠落はありません"
|
||||
},
|
||||
"library": {
|
||||
"name": "ライブラリ",
|
||||
"fields": {
|
||||
"name": "名前",
|
||||
"path": "パス",
|
||||
"remotePath": "リモートパス",
|
||||
"lastScanAt": "最終スキャン",
|
||||
"songCount": "曲数",
|
||||
"albumCount": "アルバム数",
|
||||
"artistCount": "アーティスト数",
|
||||
"totalSongs": "曲数",
|
||||
"totalAlbums": "アルバム数",
|
||||
"totalArtists": "アーティスト数",
|
||||
"totalFolders": "フォルダー数",
|
||||
"totalFiles": "ファイル数",
|
||||
"totalMissingFiles": "欠落したファイル",
|
||||
"totalSize": "合計サイズ",
|
||||
"totalDuration": "合計時間",
|
||||
"defaultNewUsers": "新規ユーザーに対するデフォルト",
|
||||
"createdAt": "作成日",
|
||||
"updatedAt": "更新日"
|
||||
},
|
||||
"sections": {
|
||||
"basic": "基本情報",
|
||||
"statistics": "統計"
|
||||
},
|
||||
"actions": {
|
||||
"scan": "ライブラリをスキャン",
|
||||
"manageUsers": "ユーザーアクセス管理",
|
||||
"viewDetails": "詳細を表示",
|
||||
"quickScan": "クイックスキャン",
|
||||
"fullScan": "フルスキャン"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "ライブラリが正常に作成されました",
|
||||
"updated": "ライブラリが正常に更新されました",
|
||||
"deleted": "ライブラリが正常に削除されました",
|
||||
"scanStarted": "スキャンを開始しました",
|
||||
"scanCompleted": "スキャンが完了しました",
|
||||
"quickScanStarted": "クイックスキャンを開始しました",
|
||||
"fullScanStarted": "フルスキャンを開始しました",
|
||||
"scanError": "スキャン開始中にエラーが発生。ログを確認してください"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "ライブラリの名前が必要です",
|
||||
"pathRequired": "ライブラリのパスが必要です",
|
||||
"pathNotDirectory": "ライブラリパスはディレクトリである必要があります",
|
||||
"pathNotFound": "ライブラリのパスが見つかりません",
|
||||
"pathNotAccessible": "ライブラリパスへアクセスできません",
|
||||
"pathInvalid": "無効なライブラリパス"
|
||||
},
|
||||
"messages": {
|
||||
"deleteConfirm": "このライブラリを削除しますか?関連する全てのデータとユーザーアクセスが削除されます。",
|
||||
"scanInProgress": "スキャン中...",
|
||||
"noLibrariesAssigned": "このユーザーに割り当てられているライブラリはありません"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -418,8 +506,12 @@
|
||||
"shareFailure": "コピーに失敗しました %{url}",
|
||||
"downloadDialogTitle": "ダウンロード %{resource} '%{name}' (%{size})",
|
||||
"shareCopyToClipboard": "クリップボードへコピー: Ctrl+C, Enter",
|
||||
"remove_missing_title": "",
|
||||
"remove_missing_content": ""
|
||||
"remove_missing_title": "欠落ファイルを削除",
|
||||
"remove_missing_content": "選択した欠落ファイルをデータベースから削除してもよろしいですか?これにより、再生数や評価を含むそれらのファイルへの参照が完全に削除されます。",
|
||||
"remove_all_missing_title": "全ての欠落ファイルを削除",
|
||||
"remove_all_missing_content": "データベースから欠落ファイルをすべて削除してもよろしいですか?これにより、再生数や評価を含むそれらのファイルへの参照が永久に削除されます。",
|
||||
"noSimilarSongsFound": "類似の曲が見つかりませんでした",
|
||||
"noTopSongsFound": "トップソングが見つかりません"
|
||||
},
|
||||
"menu": {
|
||||
"library": "ライブラリ",
|
||||
@ -448,7 +540,13 @@
|
||||
"albumList": "アルバム",
|
||||
"about": "詳細",
|
||||
"playlists": "プレイリスト",
|
||||
"sharedPlaylists": "共有プレイリスト"
|
||||
"sharedPlaylists": "共有プレイリスト",
|
||||
"librarySelector": {
|
||||
"allLibraries": "全てのライブラリ( %{count} )",
|
||||
"multipleLibraries": "%{selected} 個 / %{total} 個のライブラリ",
|
||||
"selectLibraries": "ライブラリを選択",
|
||||
"none": "無し"
|
||||
}
|
||||
},
|
||||
"player": {
|
||||
"playListsText": "再生リスト",
|
||||
@ -485,15 +583,34 @@
|
||||
"disabled": "無効",
|
||||
"waiting": "待機中"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"about": "詳細",
|
||||
"config": "設定"
|
||||
},
|
||||
"config": {
|
||||
"configName": "設定名",
|
||||
"environmentVariable": "環境変数",
|
||||
"currentValue": "現在値",
|
||||
"configurationFile": "設定ファイル",
|
||||
"exportToml": "設定をエクスポート(TOML)",
|
||||
"exportSuccess": "設定をTOML形式でクリップボードへエクスポートしました",
|
||||
"exportFailed": "設定のコピーに失敗しました",
|
||||
"devFlagsHeader": "開発フラグ(変更・削除の可能性あり)",
|
||||
"devFlagsComment": "これらは実験的な設定であり、将来のバージョンで削除される可能性があります"
|
||||
}
|
||||
},
|
||||
"activity": {
|
||||
"title": "活動",
|
||||
"totalScanned": "スキャン済みフォルダー",
|
||||
"quickScan": "クイックスキャン",
|
||||
"fullScan": "フルスキャン",
|
||||
"quickScan": "クイック",
|
||||
"fullScan": "フル",
|
||||
"serverUptime": "サーバー稼働時間",
|
||||
"serverDown": "サーバーオフライン"
|
||||
"serverDown": "サーバーオフライン",
|
||||
"scanType": "最終スキャン",
|
||||
"status": "スキャンエラー",
|
||||
"elapsedTime": "経過時間",
|
||||
"selectiveScan": "選択的スキャン"
|
||||
},
|
||||
"help": {
|
||||
"title": "ホットキー",
|
||||
@ -508,5 +625,10 @@
|
||||
"toggle_love": "星の付け外し",
|
||||
"current_song": "現在の曲へ移動"
|
||||
}
|
||||
},
|
||||
"nowPlaying": {
|
||||
"title": "再生中",
|
||||
"empty": "何も再生されていません",
|
||||
"minutesAgo": "%{smart_count} 分前 |||| %{smart_count} 分前"
|
||||
}
|
||||
}
|
||||
@ -301,14 +301,19 @@
|
||||
"actions": {
|
||||
"scan": "Skanuj Bibliotekę",
|
||||
"manageUsers": "Zarządzaj Dostępami Użytkownika",
|
||||
"viewDetails": "Zobacz Szczegóły"
|
||||
"viewDetails": "Zobacz Szczegóły",
|
||||
"quickScan": "Szybkie Skanowanie",
|
||||
"fullScan": "Pełne Skanowanie"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Biblioteka utworzona prawidłowo",
|
||||
"updated": "Biblioteka zaktualizowana prawidłowo",
|
||||
"deleted": "Biblioteka usunięta prawidłowo",
|
||||
"scanStarted": "Rozpoczęto skan biblioteki",
|
||||
"scanCompleted": "Zakończono skan biblioteki"
|
||||
"scanCompleted": "Zakończono skan biblioteki",
|
||||
"quickScanStarted": "Szybkie skanowanie rozpoczęte",
|
||||
"fullScanStarted": "Pełne skanowanie rozpoczęte",
|
||||
"scanError": "Błąd podczas startu skanowania. Sprawdź logi"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Nazwa biblioteki jest wymagana",
|
||||
@ -604,7 +609,8 @@
|
||||
"serverDown": "NIEDOSTĘPNY",
|
||||
"scanType": "Typ",
|
||||
"status": "Błąd Skanowania",
|
||||
"elapsedTime": "Upłynięty Czas"
|
||||
"elapsedTime": "Upłynięty Czas",
|
||||
"selectiveScan": "Selektywne"
|
||||
},
|
||||
"help": {
|
||||
"title": "Skróty Klawiszowe Navidrome",
|
||||
|
||||
@ -301,20 +301,25 @@
|
||||
"actions": {
|
||||
"scan": "Сканировать библиотеку",
|
||||
"manageUsers": "Управление доступом пользователей",
|
||||
"viewDetails": "Просмотреть подробности"
|
||||
"viewDetails": "Просмотреть подробности",
|
||||
"quickScan": "Быстрое сканирование",
|
||||
"fullScan": "Полное сканирование"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Библиотека успешно создана",
|
||||
"updated": "Библиотека успешно обновлена",
|
||||
"deleted": "Библиотека успешно удалена",
|
||||
"scanStarted": "Сканирование библиотеки начато",
|
||||
"scanCompleted": "Сканирование библиотеки закончено"
|
||||
"scanCompleted": "Сканирование библиотеки закончено",
|
||||
"quickScanStarted": "Быстрое сканирование началось",
|
||||
"fullScanStarted": "Началось полное сканирование",
|
||||
"scanError": "Ошибка при запуске сканирования. Проверьте логи"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Имя библиотеки обязательно",
|
||||
"pathRequired": "Путь к библиотеке обязателен",
|
||||
"pathNotDirectory": "Путь к библиотеке должен быть директорией",
|
||||
"pathNotFound": "Путь к библиотеке не найдено",
|
||||
"pathNotFound": "Путь к библиотеке не найден",
|
||||
"pathNotAccessible": "Путь к библиотеке недоступен",
|
||||
"pathInvalid": "Неверный путь к библиотеке"
|
||||
},
|
||||
@ -604,7 +609,8 @@
|
||||
"serverDown": "Оффлайн",
|
||||
"scanType": "Тип",
|
||||
"status": "Ошибка сканирования",
|
||||
"elapsedTime": "Прошедшее время"
|
||||
"elapsedTime": "Прошедшее время",
|
||||
"selectiveScan": "Избирательный"
|
||||
},
|
||||
"help": {
|
||||
"title": "Горячие клавиши Navidrome",
|
||||
|
||||
@ -301,14 +301,19 @@
|
||||
"actions": {
|
||||
"scan": "Scanna bibliotek",
|
||||
"manageUsers": "Hantera användaråtkomst",
|
||||
"viewDetails": "Se detaljer"
|
||||
"viewDetails": "Se detaljer",
|
||||
"quickScan": "Snabbscan",
|
||||
"fullScan": "Komplett scan"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Biblioteket har skapats",
|
||||
"updated": "Biblioteket har uppdaterats",
|
||||
"deleted": "Biblioteket har raderats",
|
||||
"scanStarted": "Biblioteksscan startad",
|
||||
"scanCompleted": "Biblioteksscan avslutad"
|
||||
"scanCompleted": "Biblioteksscan avslutad",
|
||||
"quickScanStarted": "Snabbscan startad",
|
||||
"fullScanStarted": "Komplett scan startad",
|
||||
"scanError": "Fel vid start av scan. Se loggarna"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Biblioteksnamn krävs",
|
||||
@ -604,7 +609,8 @@
|
||||
"serverDown": "OFFLINE",
|
||||
"scanType": "Typ",
|
||||
"status": "Fel vid scanning",
|
||||
"elapsedTime": "Spelad tid"
|
||||
"elapsedTime": "Spelad tid",
|
||||
"selectiveScan": "Urval"
|
||||
},
|
||||
"help": {
|
||||
"title": "Navidrome kortkommandon",
|
||||
|
||||
@ -301,14 +301,19 @@
|
||||
"actions": {
|
||||
"scan": "สแกนห้องสมุด",
|
||||
"manageUsers": "ตั้งค่าการเข้าถึง",
|
||||
"viewDetails": "ดูรายละเอียด"
|
||||
"viewDetails": "ดูรายละเอียด",
|
||||
"quickScan": "สแกนแบบเร็ว",
|
||||
"fullScan": "สแกนแบบเต็ม"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "สร้างห้องสมุดเรียบร้อย",
|
||||
"updated": "อัพเดทห้องสมุดเรียบร้อย",
|
||||
"deleted": "ลบห้องสมุดเพลงเรียบร้อยแล้ว",
|
||||
"scanStarted": "เริ่มสแกนห้องสมุด",
|
||||
"scanCompleted": "สแกนห้องสมุดเสร็จแล้ว"
|
||||
"scanCompleted": "สแกนห้องสมุดเสร็จแล้ว",
|
||||
"quickScanStarted": "เริ่มสแกนแบบเร็ว",
|
||||
"fullScanStarted": "เริ่มสแกนแบบเต็ม",
|
||||
"scanError": "การเริ่มสแกนผิดพลาด ดูในบันทึก"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "ต้องใส่ชื่อห้องสมุดเพลง",
|
||||
@ -604,7 +609,8 @@
|
||||
"serverDown": "ออฟไลน์",
|
||||
"scanType": "ประเภท",
|
||||
"status": "สแกนผิดพลาด",
|
||||
"elapsedTime": "เวลาที่ใช้"
|
||||
"elapsedTime": "เวลาที่ใช้",
|
||||
"selectiveScan": "เลือก"
|
||||
},
|
||||
"help": {
|
||||
"title": "คีย์ลัด Navidrome",
|
||||
|
||||
@ -301,14 +301,19 @@
|
||||
"actions": {
|
||||
"scan": "Сканувати бібліотеку",
|
||||
"manageUsers": "Керування доступом користувачів",
|
||||
"viewDetails": "Переглянути подробиці"
|
||||
"viewDetails": "Переглянути подробиці",
|
||||
"quickScan": "Швидке сканування",
|
||||
"fullScan": "Повне сканування"
|
||||
},
|
||||
"notifications": {
|
||||
"created": "Бібліотеку успішно створено",
|
||||
"updated": "Бібліотеку успішно оновлено",
|
||||
"deleted": "Бібліотеку успішно видалено",
|
||||
"scanStarted": "Сканування бібліотеки розпочато",
|
||||
"scanCompleted": "Сканування бібліотеки закінчено"
|
||||
"scanCompleted": "Сканування бібліотеки закінчено",
|
||||
"quickScanStarted": "Швидке сканування виконується",
|
||||
"fullScanStarted": "Повне сканування виконується",
|
||||
"scanError": "Помилка при виконанні сканування. Перевірте лоґи"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Ім'я бібліотеки обов'язкове",
|
||||
@ -604,7 +609,8 @@
|
||||
"serverDown": "Оффлайн",
|
||||
"scanType": "Тип",
|
||||
"status": "Помилка сканування",
|
||||
"elapsedTime": "Пройдений час"
|
||||
"elapsedTime": "Пройдений час",
|
||||
"selectiveScan": "Вибірковий"
|
||||
},
|
||||
"help": {
|
||||
"title": "Гарячі клавіші Navidrome",
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@ -69,6 +72,13 @@ func (s *Server) Run(ctx context.Context, addr string, port int, tlsCert string,
|
||||
// Determine if TLS is enabled
|
||||
tlsEnabled := tlsCert != "" && tlsKey != ""
|
||||
|
||||
// Validate TLS certificates before starting the server
|
||||
if tlsEnabled {
|
||||
if err := validateTLSCertificates(tlsCert, tlsKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a listener based on the address type (either Unix socket or TCP)
|
||||
var listener net.Listener
|
||||
var err error
|
||||
@ -89,17 +99,17 @@ func (s *Server) Run(ctx context.Context, addr string, port int, tlsCert string,
|
||||
// Start the server in a new goroutine and send an error signal to errC if there's an error
|
||||
errC := make(chan error)
|
||||
go func() {
|
||||
var err error
|
||||
if tlsEnabled {
|
||||
// Start the HTTPS server
|
||||
log.Info("Starting server with TLS (HTTPS) enabled", "tlsCert", tlsCert, "tlsKey", tlsKey)
|
||||
if err := server.ServeTLS(listener, tlsCert, tlsKey); !errors.Is(err, http.ErrServerClosed) {
|
||||
errC <- err
|
||||
}
|
||||
err = server.ServeTLS(listener, tlsCert, tlsKey)
|
||||
} else {
|
||||
// Start the HTTP server
|
||||
if err := server.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
||||
errC <- err
|
||||
}
|
||||
err = server.Serve(listener)
|
||||
}
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
errC <- err
|
||||
}
|
||||
}()
|
||||
|
||||
@ -249,3 +259,56 @@ func AbsoluteURL(r *http.Request, u string, params url.Values) string {
|
||||
}
|
||||
return buildUrl.String()
|
||||
}
|
||||
|
||||
// validateTLSCertificates validates the TLS certificate and key files before starting the server.
|
||||
// It provides detailed error messages for common issues like encrypted private keys.
|
||||
func validateTLSCertificates(certFile, keyFile string) error {
|
||||
// Read the key file to check for encryption
|
||||
keyData, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading TLS key file: %w", err)
|
||||
}
|
||||
|
||||
// Parse PEM blocks and check for encryption
|
||||
block, _ := pem.Decode(keyData)
|
||||
if block == nil {
|
||||
return errors.New("TLS key file does not contain a valid PEM block")
|
||||
}
|
||||
|
||||
// Check for encrypted private key indicators
|
||||
if isEncryptedPEM(block, keyData) {
|
||||
return errors.New("TLS private key is encrypted (password-protected). " +
|
||||
"Navidrome does not support encrypted private keys. " +
|
||||
"Please decrypt your key using: openssl pkey -in <encrypted-key> -out <decrypted-key>")
|
||||
}
|
||||
|
||||
// Try to load the certificate pair to validate it
|
||||
_, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading TLS certificate/key pair: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isEncryptedPEM checks if a PEM block represents an encrypted private key.
|
||||
func isEncryptedPEM(block *pem.Block, rawData []byte) bool {
|
||||
// Check for PKCS#8 encrypted format (BEGIN ENCRYPTED PRIVATE KEY)
|
||||
if block.Type == "ENCRYPTED PRIVATE KEY" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for legacy encrypted format with Proc-Type header
|
||||
if block.Headers != nil {
|
||||
if procType, ok := block.Headers["Proc-Type"]; ok && strings.Contains(procType, "ENCRYPTED") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Also check raw data for DEK-Info header (in case pem.Decode doesn't parse headers correctly)
|
||||
if bytes.Contains(rawData, []byte("DEK-Info:")) || bytes.Contains(rawData, []byte("Proc-Type: 4,ENCRYPTED")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@ -107,3 +114,146 @@ var _ = Describe("createUnixSocketFile", func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("TLS support", func() {
|
||||
Describe("validateTLSCertificates", func() {
|
||||
const testDataDir = "server/testdata"
|
||||
|
||||
When("certificate and key are valid and unencrypted", func() {
|
||||
It("returns nil", func() {
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
When("private key is encrypted with PKCS#8 format", func() {
|
||||
It("returns an error with helpful message", func() {
|
||||
certFile := filepath.Join(testDataDir, "test_cert_encrypted.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key_encrypted.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("encrypted"))
|
||||
Expect(err.Error()).To(ContainSubstring("openssl"))
|
||||
})
|
||||
})
|
||||
|
||||
When("private key is encrypted with legacy format (Proc-Type header)", func() {
|
||||
It("returns an error with helpful message", func() {
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key_encrypted_legacy.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("encrypted"))
|
||||
Expect(err.Error()).To(ContainSubstring("openssl"))
|
||||
})
|
||||
})
|
||||
|
||||
When("key file does not exist", func() {
|
||||
It("returns an error", func() {
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "nonexistent.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("reading TLS key file"))
|
||||
})
|
||||
})
|
||||
|
||||
When("key file does not contain valid PEM", func() {
|
||||
It("returns an error", func() {
|
||||
// Create a temp file with invalid PEM content
|
||||
tmpFile, err := os.CreateTemp("", "invalid_key*.pem")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
DeferCleanup(func() {
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
})
|
||||
_, err = tmpFile.WriteString("not a valid PEM file")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
_ = tmpFile.Close()
|
||||
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
err = validateTLSCertificates(certFile, tmpFile.Name())
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("valid PEM block"))
|
||||
})
|
||||
})
|
||||
|
||||
When("certificate file does not exist", func() {
|
||||
It("returns an error from tls.LoadX509KeyPair", func() {
|
||||
certFile := filepath.Join(testDataDir, "nonexistent_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key.pem")
|
||||
err := validateTLSCertificates(certFile, keyFile)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("loading TLS certificate/key pair"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Server TLS", func() {
|
||||
const testDataDir = "server/testdata"
|
||||
|
||||
When("server is started with valid TLS certificates", func() {
|
||||
It("accepts HTTPS connections", func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
|
||||
// Create server with mock dependencies
|
||||
ds := &tests.MockDataStore{}
|
||||
server := New(ds, nil, nil)
|
||||
|
||||
// Load the test certificate to create a trusted CA pool
|
||||
certFile := filepath.Join(testDataDir, "test_cert.pem")
|
||||
keyFile := filepath.Join(testDataDir, "test_key.pem")
|
||||
caCert, err := os.ReadFile(certFile)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
// Create an HTTPS client that trusts our test certificate
|
||||
httpClient := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Start the server in a goroutine
|
||||
ctx, cancel := context.WithCancel(GinkgoT().Context())
|
||||
defer cancel()
|
||||
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
errChan <- server.Run(ctx, "127.0.0.1", 14534, certFile, keyFile)
|
||||
}()
|
||||
|
||||
Eventually(func() error {
|
||||
// Make an HTTPS request to the server
|
||||
resp, err := httpClient.Get("https://127.0.0.1:14534/ping")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}, 2*time.Second, 100*time.Millisecond).Should(Succeed())
|
||||
|
||||
// Stop the server
|
||||
cancel()
|
||||
|
||||
// Wait for server to stop (with timeout)
|
||||
select {
|
||||
case <-errChan:
|
||||
// Server stopped
|
||||
case <-time.After(2 * time.Second):
|
||||
Fail("Server did not stop in time")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
23
server/testdata/test_cert.pem
vendored
Normal file
23
server/testdata/test_cert.pem
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDwzCCAqugAwIBAgIUXqdUxUOo8kmsDe71iTR+Vr7btP8wDQYJKoZIhvcNAQEL
|
||||
BQAwYjELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx
|
||||
EjAQBgNVBAoMCU5hdmlkcm9tZTENMAsGA1UECwwEVGVzdDESMBAGA1UEAwwJbG9j
|
||||
YWxob3N0MCAXDTI1MTEyODE5NTkxNVoYDzIxMjUxMTA0MTk1OTE1WjBiMQswCQYD
|
||||
VQQGEwJVUzENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDESMBAGA1UECgwJ
|
||||
TmF2aWRyb21lMQ0wCwYDVQQLDARUZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkB/TQgl5ei5KRSHt5OJim8rKS
|
||||
MzRlkK4BjSEM4D9ESbebdpEVjX48QuBYACrCvgvVp7mQGF5anl8Hm89trvd8ooVQ
|
||||
x9IPQQ6gRKM+4gLrt9FHvFGGzZQS8UTQXN5oBi11E+8/Vs47HLUNXC2TRtRLCMyK
|
||||
LYXQIXbhdp9anImlt+IHUxIQUchK6Zkld/gCm56X1bbzN/Zq91PQLpx2FZ0eZTjN
|
||||
KaNgztLa+K/BDnTuk3iTTs9GEp6VCvqQE/6fk/UN/tkk2dLwKIFvPVR/YeAhVdz/
|
||||
OHC4L3B36QN3+VQ2yDjsp1PVAPX07UnzXO3Oj7uGYnMQxwprGMEubm3nADDxAgMB
|
||||
AAGjbzBtMB0GA1UdDgQWBBRAZHUVuLyzc0CfuZR9ApqMbawIqzAfBgNVHSMEGDAW
|
||||
gBRAZHUVuLyzc0CfuZR9ApqMbawIqzAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQT
|
||||
MBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAmDLXcPx9LNHs
|
||||
GxQIE6Q5BXbVO7c8qrWmJf5FK5VWaifNZ9U+IBi+VlB4jCLK/OkwsviN/jOnwRYx
|
||||
owjq0QG0YdRT4uD9fEMrAj+EwbnrQYZQvT0yGEWA+KW5TW08wt+/qnGJDwEgbjYJ
|
||||
HTdICVMhs/e8Ex48fAgO8WSsdTDekOrhuwzIfeJ1LU4ZptLsD2ePFxuzutdIuW51
|
||||
/mspQGsjXqZ1qnLsavLXh/lds2g602rTpYBNZVjV9WiOvaQS8vviOxBN6f+9vgRz
|
||||
a8SEbHqBG6jeyVqVZ7MjxcYxaIkxeBwMyMwgb+wwDfVXo2FZzX2TVeB7ZppI+IKv
|
||||
TXYurWPYsQ==
|
||||
-----END CERTIFICATE-----
|
||||
22
server/testdata/test_cert_encrypted.pem
vendored
Normal file
22
server/testdata/test_cert_encrypted.pem
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDpzCCAo+gAwIBAgIUEa7gEJYwJqYEJjTY7otQ+oUyELwwDQYJKoZIhvcNAQEL
|
||||
BQAwYjELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx
|
||||
EjAQBgNVBAoMCU5hdmlkcm9tZTENMAsGA1UECwwEVGVzdDESMBAGA1UEAwwJbG9j
|
||||
YWxob3N0MCAXDTI1MTEyODE5NTI0OVoYDzIxMjUxMTA0MTk1MjQ5WjBiMQswCQYD
|
||||
VQQGEwJVUzENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDESMBAGA1UECgwJ
|
||||
TmF2aWRyb21lMQ0wCwYDVQQLDARUZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBHgqJ1d9EnNxqoSZ6xXrIz/mV
|
||||
Y0nWJW16/qIAvCdovSeTZhG9iqG8dUqcuu2BdD9MMHndJ2oFn3iD8EJR92dH8KBA
|
||||
8xOmtZ0BEEWgXPBivywZVd1ChIflEWj6m5wwLNjb57SPpUiwaLxBQB8ByEaAAZE/
|
||||
bLqvHI3vW/4s5apky17SPIqmkmqEYlRcg97tlRXsPuwoAVM9cvLMMEqtIR1CB/72
|
||||
gboY2Gi2r/plLF/Rg3Dom6QljMWi57XXWJFwGYSXaZuM0gvn04e3oLu+1E+WMoq/
|
||||
9rExWij2DlsmXd/RiScliFp6R4H84wQUyqrAUNytvgRO+oVnRjEA0l3oCYdRAgMB
|
||||
AAGjUzBRMB0GA1UdDgQWBBQQKpB1UaKm98FnBdl8uKdRscrVTzAfBgNVHSMEGDAW
|
||||
gBQQKpB1UaKm98FnBdl8uKdRscrVTzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQBP07l+2LmpFtcxqMGmsiNYwFuHpQCxJd4YRZHjLX7O+oJExMgR
|
||||
2yP4mpMKurgKOv7unTDLwvjQRa6ZTYJCsYtvC6hbyqlGc7AfNTu6DKz8r35/2/V5
|
||||
hPsG5lNb91HhvHE839mLAvpi02LoFH2Sr8BR7s6qxfNKYcP8PUOJQXltJ6yAa8YJ
|
||||
syeXQQ3RIyGsJANeaC06S3UdkBM5H5BLfIHnHu3GybJjwL51va4WCdHe8QV6GI0g
|
||||
RDiThDVkBSXAr136vnMdlrYCxMoxY56itJ0zbYg2ELQKU9o1w/ZJQo9uvmy9jCoZ
|
||||
Hy1L5a2vUDbsdONdvRkYZRHqMpG4bdD8D3j2
|
||||
-----END CERTIFICATE-----
|
||||
28
server/testdata/test_key.pem
vendored
Normal file
28
server/testdata/test_key.pem
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkB/TQgl5ei5KR
|
||||
SHt5OJim8rKSMzRlkK4BjSEM4D9ESbebdpEVjX48QuBYACrCvgvVp7mQGF5anl8H
|
||||
m89trvd8ooVQx9IPQQ6gRKM+4gLrt9FHvFGGzZQS8UTQXN5oBi11E+8/Vs47HLUN
|
||||
XC2TRtRLCMyKLYXQIXbhdp9anImlt+IHUxIQUchK6Zkld/gCm56X1bbzN/Zq91PQ
|
||||
Lpx2FZ0eZTjNKaNgztLa+K/BDnTuk3iTTs9GEp6VCvqQE/6fk/UN/tkk2dLwKIFv
|
||||
PVR/YeAhVdz/OHC4L3B36QN3+VQ2yDjsp1PVAPX07UnzXO3Oj7uGYnMQxwprGMEu
|
||||
bm3nADDxAgMBAAECggEABqJFvesP2v4FEvgd+kSWM+ZL34rPmy3zQ5/MDuPA20ep
|
||||
89EjQ/5hdRl1TknPcOnTu7PZVuENa9fM2xdrl7GEU9eU0bQLJE/KwiOUgJYObS8V
|
||||
eTO+DlghHXUBhfXDjux1CS+htOuTUqOyFNS+CR9Lta8o6ou1xjmcP7kW78i17mxF
|
||||
TuH5SZlS8W9PFLXHCInbMtqGFaT2ss09kvoPk2FDvHfxEdy6M9tKkguz02g+4bqI
|
||||
aAMp2N7AOfmRpC0HvVa1ZfZo5Z8/KMoNcIm3pV9DEVM369J9EzhnMNpkGben90aT
|
||||
FqO2JNsy52wmXFZUc9xe8uPdfDahALCkBGncLyLNmQKBgQDZREjocjdzOoPSlCdx
|
||||
mRNe9suHz2FpUpsHCPOCotG63hFVKpah/ZvpHSsQx5rXs/mawDTmzGY9GQiBrSvg
|
||||
OhfHIyT3NOhVaNcMxTqJX7rs7OG8D0MBacD9ASSeZ89MUn8q1EHZr5qxLtXl5Ikw
|
||||
mHtiGRdiKGFFrG9H0zncbGhy7QKBgQDBRhQ9RAasTdmUiNQly9GVFkXto4T/9UHx
|
||||
rVU44htCI2IVZUMTGlNfclfxpByDrzyA56rMzN9SAkiIp4nPpMDs5hayXaaPoojs
|
||||
CPzV7r2OjemZ6CTeQ1ODImRL8L/E3jJSgWd6YYoHSQ5hjEX4yT6ft0u0tZUfdMKd
|
||||
VENWIJ/hlQKBgQCo2hXjeOi5R8+tN3EUKwhP9HOnX7dv+D/9jqpZa5qdpPpJeyjI
|
||||
SmYCHKYci1Q+sWOaLiiu+km20B65UVFZGSzjmd+fs+GghzMifKGKo/iNK2ggFKhZ
|
||||
j8vplRrVdQ45XZ/xNDbdLEmHzEN2QE+Skd7KFYADzCgU0vdFFdbRBPuD3QKBgGIq
|
||||
fQctMRJ9LCE0akSURGwr9vKflmMHKCpfdqTAu0WZgS0K1Mm0GlqlUiPKzizYaauz
|
||||
f14sRNV7kWnPZsDPlqn8p9SKmpnj3RW97uWeMCtiyx6/+VHm8ljts/GaY1zT2s1r
|
||||
KqrPNfNDWQmU3MljNeqbh9lOTWK/xEVy0gzB31MNAoGAQNWrZvVdAbL95XW6STUu
|
||||
JmQlqJTlluuqS0Rrd/uVEQwW0Vd1dZjRQcFAFiSiCQWTbtId5gFZd6hiIQl53Xz0
|
||||
5cd+9mcyA/TaoCJYbMOFYsKbZMCBhefsovJlVQXedqJrIY6BdeGlet4GTAH5Qyl0
|
||||
ytEIUnvn5YmmbI7PDz80XpU=
|
||||
-----END PRIVATE KEY-----
|
||||
30
server/testdata/test_key_encrypted.pem
vendored
Normal file
30
server/testdata/test_key_encrypted.pem
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
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
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
30
server/testdata/test_key_encrypted_legacy.pem
vendored
Normal file
30
server/testdata/test_key_encrypted_legacy.pem
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,3C969050EAB73F121B7F0E6B75C42525
|
||||
|
||||
V6pSaAsrn9CQNo4p88QshJLbg8zkQJEom81dPbYSVqQSZa9YlPtpLZ9YtuLj/Ay0
|
||||
TScEKIj/gzQ32wNl6nhcSNIL9yy+X11r5gNv1kIHkecf+EbDW20VOiJsfD+6LUyW
|
||||
hA96AIbPOwc76iCuvsKHPKU9MlEmjGipmk/C2RQLHCZJ3WkiDRgCM8KQ7vKhfACT
|
||||
w908yj4cB1e/P0JPq8t/3F7kPJ+6SVM1vMEffHl0otQR3rAyrK8QikwJ0K9qX62d
|
||||
cqchTVlEyyZBYovR8DrRRUDbsXS5j1ZmX3NQpvTSTFowr+33fMrY+4Oz8sdR4yx1
|
||||
CQc0A0sHHxSEIr2xu4KzczwOYVJN8PVdU0pgvFj9KEm66N6EY5CSFIBHyO/ycOt9
|
||||
U+wpkRjf3zS6ZaUU0NKdOcop4YX33i99/tZF2RNR1i7ETLYph+/LCf09286Bi3u/
|
||||
UCCuWedyECPdz0c6j0s27Fdfc/HEK90OEzeWh/fc+H2gJZhqJYK9V47HPTQNNMnB
|
||||
U1a6FsJlrKE3E6nfSnTLxrSx9m/XTV7HV+HkgX+q8VhN7Q2VHUqkPzE7ZOPYpZ+A
|
||||
dQzsm1TmEMxym6osYqFzQScXR1NZasrV2MTQ2J16dUgCdGAM2YMUD9JaoJR+u77M
|
||||
WAjYzDiRg84rLr/KbJPAwHbsfo2KpiapJGSBBEDhz4W1/LOrFhsjaqIMSy4yZDGm
|
||||
1KqXGHIlqmuHI7v4fD8vuzhj7GUujRx85HSZWakE/uc6s5WrhkSeVKYJWPfpsxTv
|
||||
dT3oLOGJ+nRzWxM3aFtuJghX0nIGdKxT4EAUNXz0/vLT3OP1QCZR+oELrriFzmtj
|
||||
+O30bGH2SAFZEQJ/uTQg6celoNh89IzH4DJkcn67hqpX6mUiU9CrIr/eR9C/en8Q
|
||||
smTbbC1C1pDUaCwR26Z+zgM90amh4yfOFKK2geO2Kj+TmwFHUvi6ZnSzMzCvty3t
|
||||
+wdIrUtf55Lw51JCpLGl70mg4b/zBj5hqBkU2YvAAnz/htjfH/wrD6ZAF1TCdlRO
|
||||
gyODrJjGRnLd/v0XLk0wp+RkAjBcSlRlkUvZY5BtugL7dIdwiNGGQPcOni9IVeG0
|
||||
6vDUEQnDOLYDj4d/JcckTLuHdrP+SW+0RQl2HK5+/w1hScGXN4O48gccu7yR/MN8
|
||||
DmpCg5rD/nq8sxJosmSt07GrN36KppYt8LCXQbSg3NG2Ad715caS2C+0Qtdm5MPD
|
||||
rM1UyTXQYSJXgUN9yZS/pmzlguCywnnvsBPU6j3ljZwcoD41QJ/1OU09/W6sIMQR
|
||||
IAiM35JHiLJiccFgxSE1qx5F1UZqX4P47jF0Wzi/sE/DYXg5qw2DoauqXNzqnumH
|
||||
71UDGK1V6wQIV7UCZDa0WUfFzu470XpuFb8VmMOuHSQxkZESc9cz8k/ueAuO438Q
|
||||
jnlkF1Ge2EEPuaK2zeaTj/lGyYA1AUfHRRgt/EMUQSBntmhlpnwVPYTVvYtHO2N5
|
||||
wp7/y39KirnlTl99i3XiOJ4WF4gIU2IaSlqMo4+e/A32h2JFi9QfNyfItXe6Fm1X
|
||||
d0j2XGHzwMfHEFKdWyrgtVZwc38/1d6xWYAhs02b2basV/0AQhFTaKf5Z268eBNJ
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@ -53,6 +53,7 @@ const SharePlayer = () => {
|
||||
remove: false,
|
||||
spaceBar: true,
|
||||
volumeFade: { fadeIn: 200, fadeOut: 200 },
|
||||
sortableOptions: { delay: 200, delayOnTouchOnly: true },
|
||||
}
|
||||
return (
|
||||
<ReactJkMusicPlayer
|
||||
|
||||
@ -47,17 +47,15 @@ const stylesheet = `
|
||||
.react-jinke-music-player-main .music-player-panel,
|
||||
.react-jinke-music-player-mobile,
|
||||
.ril__outer{
|
||||
background-color: #1f1f1f;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #fff1;
|
||||
}
|
||||
.ril__toolbar{
|
||||
background-color: #1d1d1d
|
||||
}
|
||||
.ril__toolbarItem{
|
||||
font-size: 100%;
|
||||
color: #eee
|
||||
}
|
||||
.audio-lists-panel{
|
||||
.audio-lists-panel,
|
||||
.ril__toolbar{
|
||||
background-color: #1f1f1f;
|
||||
border: 1px solid #fff1;
|
||||
border-radius: 6px 6px 0 0;
|
||||
|
||||
@ -137,22 +137,19 @@ export default {
|
||||
albumName: {
|
||||
color: '#eee',
|
||||
},
|
||||
albumSubtitle: {
|
||||
color: '#ccc',
|
||||
},
|
||||
albumPlayButton: {
|
||||
color: '#ff4e6b !important',
|
||||
color: '#ff4e6b',
|
||||
},
|
||||
albumArtistName: {
|
||||
color: '#ff4e6b !important',
|
||||
color: '#ccc',
|
||||
},
|
||||
cover: {
|
||||
borderRadius: '10px !important',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
},
|
||||
NDLogin: {
|
||||
systemNameLink: {
|
||||
color: '#D60017',
|
||||
color: '#ff4e6b',
|
||||
},
|
||||
welcome: {
|
||||
color: '#eee',
|
||||
@ -161,6 +158,9 @@ export default {
|
||||
minWidth: 300,
|
||||
backgroundColor: '#1d1d1d',
|
||||
},
|
||||
icon: {
|
||||
filter: 'hue-rotate(115deg)',
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
elevation1: {
|
||||
@ -169,6 +169,9 @@ export default {
|
||||
root: {
|
||||
color: '#eee',
|
||||
},
|
||||
rounded: {
|
||||
borderRadius: '6px',
|
||||
},
|
||||
},
|
||||
NDMobileArtistDetails: {
|
||||
bgContainer: {
|
||||
@ -189,6 +192,30 @@ export default {
|
||||
paddingBottom: '1rem',
|
||||
},
|
||||
},
|
||||
RaDeleteWithConfirmButton: {
|
||||
deleteButton: {
|
||||
color: 'unset',
|
||||
},
|
||||
},
|
||||
RaPaginationActions: {
|
||||
currentPageButton: {
|
||||
border: '2px solid #D60017',
|
||||
background: 'transparent',
|
||||
},
|
||||
button: {
|
||||
border: '2px solid #D60017',
|
||||
},
|
||||
actions: {
|
||||
'@global': {
|
||||
'.next-page': {
|
||||
border: '0 none',
|
||||
},
|
||||
'.previous-page': {
|
||||
border: '0 none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
player: {
|
||||
theme: 'dark',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user