1.1.0 - Translations, downloads, bug fixes

This commit is contained in:
exttex 2020-10-31 16:54:28 +01:00
parent 2f8bca3dfb
commit fbafb4286d
63 changed files with 3820 additions and 636 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ app/dist/
app/client/node_modules/ app/client/node_modules/
electron_dist/ electron_dist/
freezer-*.tgz freezer-*.tgz
translations.zip

View File

@ -10,6 +10,6 @@ module.exports = {
"ecmaVersion": 12 "ecmaVersion": 12
}, },
"rules": { "rules": {
"allowEmptyCatch": true "allowEmptyCatch": 0
} }
}; };

2
app/client/.env Normal file
View File

@ -0,0 +1,2 @@
VUE_APP_I18N_LOCALE=en
VUE_APP_I18N_FALLBACK_LOCALE=en

View File

@ -80,6 +80,27 @@
"postcss": "^7.0.0" "postcss": "^7.0.0"
} }
}, },
"@intlify/vue-i18n-loader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@intlify/vue-i18n-loader/-/vue-i18n-loader-1.0.0.tgz",
"integrity": "sha512-y7LlpKEQ01u7Yq14l4VNlbFYEHMmSEH1QXXASOMWspj9ZcIdCebhhvHCHqk5Oy5Epw3PtoxyRJNpb6Wle5udgA==",
"dev": true,
"requires": {
"js-yaml": "^3.13.1",
"json5": "^2.1.1"
},
"dependencies": {
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
}
}
},
"@mdi/font": { "@mdi/font": {
"version": "5.5.55", "version": "5.5.55",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-5.5.55.tgz", "resolved": "https://registry.npmjs.org/@mdi/font/-/font-5.5.55.tgz",
@ -2170,6 +2191,50 @@
"integrity": "sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==", "integrity": "sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==",
"dev": true "dev": true
}, },
"cli-table3": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz",
"integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==",
"dev": true,
"requires": {
"colors": "^1.1.2",
"object-assign": "^4.1.0",
"string-width": "^2.1.1"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
}
}
},
"cli-width": { "cli-width": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
@ -2288,6 +2353,13 @@
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
"dev": true "dev": true
}, },
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"dev": true,
"optional": true
},
"combined-stream": { "combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -3322,6 +3394,16 @@
"domelementtype": "1" "domelementtype": "1"
} }
}, },
"dot-object": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/dot-object/-/dot-object-1.9.0.tgz",
"integrity": "sha512-7MPN6y7XhAO4vM4eguj5+5HNKLjJYfkVG1ZR1Aput4Q4TR6SYeSjhpVQ77IzJHoSHffKbDxBC+48aCiiRurDPw==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"glob": "^7.1.4"
}
},
"dot-prop": { "dot-prop": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
@ -3765,6 +3847,12 @@
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"dev": true "dev": true
}, },
"esm": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
"dev": true
},
"espree": { "espree": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
@ -4264,6 +4352,12 @@
"pinkie-promise": "^2.0.0" "pinkie-promise": "^2.0.0"
} }
}, },
"flat": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
"dev": true
},
"flat-cache": { "flat-cache": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
@ -5524,6 +5618,12 @@
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
"dev": true "dev": true
}, },
"is-valid-glob": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz",
"integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=",
"dev": true
},
"is-windows": { "is-windows": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@ -9783,6 +9883,54 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==" "integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
}, },
"vue-cli-plugin-i18n": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-i18n/-/vue-cli-plugin-i18n-1.0.1.tgz",
"integrity": "sha512-sLo6YzudaWgn5dOMvrKixE5bb/onYGxcxm+0YexqoOx0QtR+7hZ/P5WPFBMM9v/2i1ec2YYe2PvKTBel7KE+tA==",
"dev": true,
"requires": {
"debug": "^4.1.0",
"deepmerge": "^4.2.0",
"dotenv": "^8.2.0",
"flat": "^5.0.0",
"rimraf": "^3.0.0",
"vue": "^2.6.11",
"vue-i18n": "^8.17.0",
"vue-i18n-extract": "1.0.2"
},
"dependencies": {
"debug": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"vue-cli-plugin-vuetify": { "vue-cli-plugin-vuetify": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.7.tgz", "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.0.7.tgz",
@ -9854,6 +10002,118 @@
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==",
"dev": true "dev": true
}, },
"vue-i18n": {
"version": "8.22.1",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.22.1.tgz",
"integrity": "sha512-JNgiEJ5a8YPfk5y2lKyfOAGLmkpAVfhaUi+T4wGpSppRYZ3XSyawSDDketY5KV2CsAiBLAGEIO6jO+0l2hQubg=="
},
"vue-i18n-extract": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/vue-i18n-extract/-/vue-i18n-extract-1.0.2.tgz",
"integrity": "sha512-+zwDKvle4KcfloXZnj5hF01ViKDiFr5RMx5507D7oyDXpSleRpekF5YHgZa/+Ra6Go68//z0Nya58J9tKFsCjw==",
"dev": true,
"requires": {
"cli-table3": "^0.5.1",
"dot-object": "^1.7.1",
"esm": "^3.2.13",
"glob": "^7.1.3",
"is-valid-glob": "^1.0.0",
"yargs": "^13.2.2"
},
"dependencies": {
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"vue-loader": { "vue-loader": {
"version": "15.9.3", "version": "15.9.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.3.tgz", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.3.tgz",

View File

@ -5,8 +5,9 @@
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"watch": "vue-cli-service build --watch", "lint": "vue-cli-service lint",
"lint": "vue-cli-service lint" "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'",
"watch": "vue-cli-service build --watch"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^5.5.55", "@mdi/font": "^5.5.55",
@ -14,11 +15,13 @@
"roboto-fontface": "*", "roboto-fontface": "*",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-esc": "^3.0.1", "vue-esc": "^3.0.1",
"vue-i18n": "^8.17.3",
"vue-router": "^3.2.0", "vue-router": "^3.2.0",
"vue-socket.io": "^3.0.10", "vue-socket.io": "^3.0.10",
"vuetify": "^2.2.11" "vuetify": "^2.2.11"
}, },
"devDependencies": { "devDependencies": {
"@intlify/vue-i18n-loader": "^1.0.0",
"@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0", "@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.0",
@ -26,6 +29,7 @@
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"sass": "^1.19.0", "sass": "^1.19.0",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"vue-cli-plugin-i18n": "~1.0.1",
"vue-cli-plugin-vuetify": "~2.0.7", "vue-cli-plugin-vuetify": "~2.0.7",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.3.0" "vuetify-loader": "^1.3.0"

View File

@ -31,7 +31,7 @@
<v-list-item-icon> <v-list-item-icon>
<v-icon>mdi-home</v-icon> <v-icon>mdi-home</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title>Home</v-list-item-title> <v-list-item-title>{{$t('Home')}}</v-list-item-title>
</v-list-item> </v-list-item>
<!-- Browse link --> <!-- Browse link -->
@ -39,10 +39,10 @@
<v-list-item-icon> <v-list-item-icon>
<v-icon>mdi-earth</v-icon> <v-icon>mdi-earth</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title>Browse</v-list-item-title> <v-list-item-title>{{$t('Browse')}}</v-list-item-title>
</v-list-item> </v-list-item>
<v-subheader inset>Library</v-subheader> <v-subheader inset>{{$t('Library')}}</v-subheader>
<v-divider></v-divider> <v-divider></v-divider>
<!-- Tracks --> <!-- Tracks -->
@ -50,7 +50,7 @@
<v-list-item-icon> <v-list-item-icon>
<v-icon>mdi-music-note</v-icon> <v-icon>mdi-music-note</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title>Tracks</v-list-item-title> <v-list-item-title>{{$t('Tracks')}}</v-list-item-title>
</v-list-item> </v-list-item>
<!-- Playlists --> <!-- Playlists -->
@ -58,7 +58,7 @@
<v-list-item-icon> <v-list-item-icon>
<v-icon>mdi-playlist-music</v-icon> <v-icon>mdi-playlist-music</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title>Playlists</v-list-item-title> <v-list-item-title>{{$t('Playlists')}}</v-list-item-title>
</v-list-item> </v-list-item>
<!-- Albums --> <!-- Albums -->
@ -66,7 +66,7 @@
<v-list-item-icon> <v-list-item-icon>
<v-icon>mdi-album</v-icon> <v-icon>mdi-album</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title>Albums</v-list-item-title> <v-list-item-title>{{$t('Albums')}}</v-list-item-title>
</v-list-item> </v-list-item>
<!-- Artists --> <!-- Artists -->
@ -74,10 +74,10 @@
<v-list-item-icon> <v-list-item-icon>
<v-icon>mdi-account-music</v-icon> <v-icon>mdi-account-music</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title>Artists</v-list-item-title> <v-list-item-title>{{$t('Artists')}}</v-list-item-title>
</v-list-item> </v-list-item>
<v-subheader inset>More</v-subheader> <v-subheader inset>{{$t('More')}}</v-subheader>
<v-divider></v-divider> <v-divider></v-divider>
<!-- Settings --> <!-- Settings -->
@ -85,30 +85,30 @@
<v-list-item-icon> <v-list-item-icon>
<v-icon>mdi-cog</v-icon> <v-icon>mdi-cog</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title>Settings</v-list-item-title> <v-list-item-title>{{$t('Settings')}}</v-list-item-title>
</v-list-item> </v-list-item>
<!-- Downloads --> <!-- Downloads, shitty hack if downloads not yet loaded -->
<v-list-item link to='/downloads'> <v-list-item link to='/downloads' v-if='$root.downloads.queue'>
<!-- Download icon --> <!-- Download icon -->
<v-list-item-icon v-if='!$root.download && $root.downloads.length == 0'> <v-list-item-icon v-if='!$root.downloads.downloading && $root.downloads.queue.length == 0'>
<v-icon>mdi-download</v-icon> <v-icon>mdi-download</v-icon>
</v-list-item-icon> </v-list-item-icon>
<!-- Paused download --> <!-- Paused download -->
<v-list-item-icon v-if='!$root.download && $root.downloads.length > 0'> <v-list-item-icon v-if='!$root.downloads.downloading && $root.downloads.queue.length > 0'>
<v-icon>mdi-pause</v-icon> <v-icon>mdi-pause</v-icon>
</v-list-item-icon> </v-list-item-icon>
<!-- Download in progress --> <!-- Download in progress -->
<v-list-item-icon v-if='$root.download'> <v-list-item-icon v-if='$root.downloads.downloading'>
<v-progress-circular :value='downloadPercentage' style='top: -2px' class='text-caption'> <v-progress-circular :value='downloadPercentage' style='top: -2px' class='text-caption'>
{{$root.downloads.length + 1}} {{$root.downloads.queue.length + $root.downloads.threads.length}}
</v-progress-circular> </v-progress-circular>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title>Downloads</v-list-item-title> <v-list-item-title>{{$t('Downloads')}}</v-list-item-title>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -133,7 +133,7 @@
solo solo
clearable clearable
hide-no-data hide-no-data
placeholder='Search or paste Deezer URL. Use "/" to quickly focus.' :placeholder='$t("Search or paste Deezer URL. Use / to quickly focus.")'
:loading='searchLoading' :loading='searchLoading'
@keyup='search' @keyup='search'
ref='searchBar' ref='searchBar'
@ -142,6 +142,7 @@
:items='suggestions' :items='suggestions'
></v-autocomplete> ></v-autocomplete>
</v-app-bar> </v-app-bar>
<!-- Main --> <!-- Main -->
@ -355,12 +356,19 @@ export default {
}, },
computed: { computed: {
qualityText() { qualityText() {
return `${this.$root.playbackInfo.format} ${this.$root.playbackInfo.quality}`; return `${this.$root.playbackInfo.qualityString}`;
}, },
downloadPercentage() { downloadPercentage() {
if (!this.$root.download) return 0; if (!this.$root.downloads.downloading) return 0;
let p = (this.$root.download.downloaded / this.$root.download.size) * 100;
if (isNaN(p)) return 0; let downloaded = this.$root.downloads.threads.reduce((a, b) => a + b.downloaded, 0);
let size = this.$root.downloads.threads.reduce((a, b) => a + b.size, 0);
if (size == 0)
size = 1;
let p = (downloaded / size) * 100;
if (p > 100)
p = 100;
return Math.round(p); return Math.round(p);
} }
}, },
@ -413,6 +421,8 @@ export default {
this.suggestions = []; this.suggestions = [];
return; return;
} }
if (!this.$root.settings.showAutocomplete) return;
this.searchLoading = true; this.searchLoading = true;
//Prevent spam //Prevent spam
setTimeout(() => { setTimeout(() => {

View File

@ -33,7 +33,7 @@
<v-icon>mdi-play</v-icon> <v-icon>mdi-play</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Play</v-list-item-title> <v-list-item-title>{{$t("Play")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Add to library --> <!-- Add to library -->
@ -42,7 +42,7 @@
<v-icon>mdi-heart</v-icon> <v-icon>mdi-heart</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Add to library</v-list-item-title> <v-list-item-title>{{$t("Add to library")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Download --> <!-- Download -->
@ -51,7 +51,7 @@
<v-icon>mdi-download</v-icon> <v-icon>mdi-download</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Download</v-list-item-title> <v-list-item-title>{{$t("Download")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>

View File

@ -6,7 +6,7 @@
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>{{artist.name}}</v-list-item-title> <v-list-item-title>{{artist.name}}</v-list-item-title>
<v-list-item-subtitle v-if='!tiny'>{{$abbreviation(artist.fans)}} fans</v-list-item-subtitle> <v-list-item-subtitle v-if='!tiny'>{{$abbreviation(artist.fans)}} {{$t("fans")}}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<!-- Context menu --> <!-- Context menu -->
@ -23,7 +23,7 @@
<v-icon>mdi-heart</v-icon> <v-icon>mdi-heart</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Add to library</v-list-item-title> <v-list-item-title>{{$t("Add to library")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>

View File

@ -5,29 +5,29 @@
<v-card> <v-card>
<v-card-title class='headline'> <v-card-title class='headline'>
Download {{tracks.length}} tracks {{$t("Download")}} {{tracks.length}} {{$t("tracks")}}
</v-card-title> </v-card-title>
<v-card-text class='pb-0'> <v-card-text class='pb-0'>
<v-select <v-select
label='Quality' :label='$t("Quality")'
persistent-hint persistent-hint
:items='qualities' :items='qualities'
v-model='qualityString' v-model='qualityString'
:hint='"Estimated size: " + $filesize(estimatedSize)' :hint='$t("Estimated size:") + " " + $filesize(estimatedSize)'
></v-select> ></v-select>
<v-checkbox <v-checkbox
v-model='autostart' v-model='autostart'
label='Start downloading' :label='$t("Start downloading")'
></v-checkbox> ></v-checkbox>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn text @click='$emit("close")'>Cancel</v-btn> <v-btn text @click='$emit("close")'>{{$t("Cancel")}}</v-btn>
<v-btn text @click='download'>Download</v-btn> <v-btn text @click='download'>{{$t("Download")}}</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>

View File

@ -12,10 +12,10 @@
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title> <v-list-item-title>
Streaming logging is disabled! {{$t("Stream logging is disabled!")}}
</v-list-item-title> </v-list-item-title>
<v-list-item-subtitle> <v-list-item-subtitle>
Enable it in settings for history to work properly. {{$t("Enable it in settings for history to work properly.")}}
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@ -56,7 +56,7 @@ export default {
//Load as queue and play //Load as queue and play
play(index) { play(index) {
this.$root.queue.source = { this.$root.queue.source = {
text: 'History', text: this.$t('History'),
source: 'history', source: 'history',
data: null data: null
}; };

View File

@ -8,7 +8,7 @@
<!-- Create playlist --> <!-- Create playlist -->
<v-btn class='ma-2 ml-3' color='primary' @click='popup = true'> <v-btn class='ma-2 ml-3' color='primary' @click='popup = true'>
<v-icon left>mdi-playlist-plus</v-icon> <v-icon left>mdi-playlist-plus</v-icon>
Create new playlist {{$t("Create new playlist")}}
</v-btn> </v-btn>
<v-dialog max-width="400px" v-model='popup'> <v-dialog max-width="400px" v-model='popup'>

View File

@ -2,10 +2,10 @@
<div v-scroll.self='scroll'> <div v-scroll.self='scroll'>
<div class='px-4 pt-2 d-flex' style='max-height: 50px;'> <div class='px-4 pt-2 d-flex' style='max-height: 50px;'>
<div class='text-overline px-2 pt-1'> <div class='text-overline px-2 pt-1'>
{{count}} TRACKS. {{count}} {{$t("TRACKS")}}
</div> </div>
<div style="max-width: 200px;" class='d-flex mx-2'> <div style="max-width: 200px;" class='d-flex mx-2'>
<v-select class='px-2' dense solo :items='sortTypes' @change='sort' label='Sort By'> <v-select class='px-2' dense solo :items='sortTypes' @change='sort' :label='$t("Sort by")'>
</v-select> </v-select>
</div> </div>
<div class='px-2' @click='reverseSort'> <div class='px-2' @click='reverseSort'>
@ -46,10 +46,10 @@ export default {
tracks: [], tracks: [],
count: 0, count: 0,
sortTypes: [ sortTypes: [
'Date Added', this.$t('Date Added'),
'Name (A-Z)', this.$t('Name (A-Z)'),
'Artist (A-Z)', this.$t('Artist (A-Z)'),
'Album (A-Z)' this.$t('Album (A-Z)')
], ],
tracksUnsorted: null, tracksUnsorted: null,
isReversed: false isReversed: false

View File

@ -34,7 +34,7 @@
<!-- Error --> <!-- Error -->
<div v-if='!loading && (!lyrics || (lyrics.text.length == 0 && lyrics.lyrics.length == 0))' class='pa-4 text-center'> <div v-if='!loading && (!lyrics || (lyrics.text.length == 0 && lyrics.lyrics.length == 0))' class='pa-4 text-center'>
<span class='red--text text-h5'> <span class='red--text text-h5'>
Error loading lyrics or lyrics not found! {{$t("Error loading lyrics or lyrics not found!")}}
</span> </span>
</div> </div>

View File

@ -4,7 +4,7 @@
<!-- Create playlist --> <!-- Create playlist -->
<v-card class='text-center pa-2' v-if='!addToPlaylist'> <v-card class='text-center pa-2' v-if='!addToPlaylist'>
<v-card-text> <v-card-text>
<p primary-title class='display-1'>Create playlist</p> <p primary-title class='display-1'>{{$t("Create playlist")}}</p>
<v-text-field label='Title' class='ma-2' v-model='title'></v-text-field> <v-text-field label='Title' class='ma-2' v-model='title'></v-text-field>
<v-textarea class='mx-2' v-model='description' label='Description' rows='1' auto-grow></v-textarea> <v-textarea class='mx-2' v-model='description' label='Description' rows='1' auto-grow></v-textarea>
<v-select class='mx-2' v-model='type' :items='types' label='Type'></v-select> <v-select class='mx-2' v-model='type' :items='types' label='Type'></v-select>
@ -12,17 +12,17 @@
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn class='primary' :loading='createLoading' @click='create'>Create</v-btn> <v-btn class='primary' :loading='createLoading' @click='create'>{{$t("Create")}}</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
<!-- Add to playlist --> <!-- Add to playlist -->
<v-card class='text-center pa-2' v-if='addToPlaylist'> <v-card class='text-center pa-2' v-if='addToPlaylist'>
<v-card-text> <v-card-text>
<p primary-title class='display-1'>Add to playlist</p> <p primary-title class='display-1'>{{$t("Add to playlist")}}</p>
<v-btn block class='mb-1' @click='addToPlaylist = false'> <v-btn block class='mb-1' @click='addToPlaylist = false'>
<v-icon left>mdi-playlist-plus</v-icon> <v-icon left>mdi-playlist-plus</v-icon>
Create New {{$t("Create new")}}
</v-btn> </v-btn>
<v-list> <v-list>
<div v-for='playlist in playlists' :key='playlist.id'> <div v-for='playlist in playlists' :key='playlist.id'>

View File

@ -15,7 +15,7 @@
<v-list-item-content> <v-list-item-content>
<v-list-item-title>{{playlist.title}}</v-list-item-title> <v-list-item-title>{{playlist.title}}</v-list-item-title>
<v-list-item-subtitle>{{$numberString(playlist.trackCount)}} tracks</v-list-item-subtitle> <v-list-item-subtitle>{{$numberString(playlist.trackCount)}} {{$t("tracks")}}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<!-- Context menu --> <!-- Context menu -->
@ -32,7 +32,7 @@
<v-icon>mdi-play</v-icon> <v-icon>mdi-play</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Play</v-list-item-title> <v-list-item-title>{{$t('Play')}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@ -42,7 +42,7 @@
<v-icon>mdi-playlist-remove</v-icon> <v-icon>mdi-playlist-remove</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Remove</v-list-item-title> <v-list-item-title>{{$t('Remove')}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@ -52,7 +52,7 @@
<v-icon>mdi-download</v-icon> <v-icon>mdi-download</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Download</v-list-item-title> <v-list-item-title>{{$t('Download')}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>

View File

@ -45,7 +45,7 @@
<v-icon>mdi-playlist-plus</v-icon> <v-icon>mdi-playlist-plus</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Play next</v-list-item-title> <v-list-item-title>{{$t("Play next")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Add to end of queue --> <!-- Add to end of queue -->
@ -54,7 +54,7 @@
<v-icon>mdi-playlist-plus</v-icon> <v-icon>mdi-playlist-plus</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Add to queue</v-list-item-title> <v-list-item-title>{{$t("Add to queue")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Add to library --> <!-- Add to library -->
@ -63,7 +63,7 @@
<v-icon>mdi-heart</v-icon> <v-icon>mdi-heart</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Add to library</v-list-item-title> <v-list-item-title>{{$t("Add to library")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Remove from library --> <!-- Remove from library -->
@ -72,7 +72,7 @@
<v-icon>mdi-heart-remove</v-icon> <v-icon>mdi-heart-remove</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Remove from library</v-list-item-title> <v-list-item-title>{{$t("Remove from library")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Add to playlist --> <!-- Add to playlist -->
@ -81,7 +81,7 @@
<v-icon>mdi-playlist-plus</v-icon> <v-icon>mdi-playlist-plus</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Add to playlist</v-list-item-title> <v-list-item-title>{{$t("Add to playlist")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Remove from playlist --> <!-- Remove from playlist -->
@ -90,7 +90,16 @@
<v-icon>mdi-playlist-remove</v-icon> <v-icon>mdi-playlist-remove</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Remove from playlist</v-list-item-title> <v-list-item-title>{{$t("Remove from playlist")}}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Play track mix -->
<v-list-item dense @click='trackMix'>
<v-list-item-icon>
<v-icon>mdi-playlist-music</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{$t("Play track mix")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Go to album --> <!-- Go to album -->
@ -99,7 +108,7 @@
<v-icon>mdi-album</v-icon> <v-icon>mdi-album</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Go to "{{track.album.title}}"</v-list-item-title> <v-list-item-title>{{$t("Go to")}} "{{track.album.title}}"</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Go to artists --> <!-- Go to artists -->
@ -113,7 +122,7 @@
<v-icon>mdi-account-music</v-icon> <v-icon>mdi-account-music</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Go to "{{artist.name}}"</v-list-item-title> <v-list-item-title>{{$t("Go to")}} "{{artist.name}}"</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@ -123,7 +132,7 @@
<v-icon>mdi-download</v-icon> <v-icon>mdi-download</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Download</v-list-item-title> <v-list-item-title>{{$t("Download")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@ -207,8 +216,18 @@ export default {
this.$emit('remove'); this.$emit('remove');
}, },
//Download track //Download track
async download() { download() {
this.downloadDialog = true; this.downloadDialog = true;
},
async trackMix() {
let res = await this.$axios.get('/trackmix/' + this.track.id);
this.$root.queue.source = {
text: this.$t('Track Mix'),
source: 'trackmix',
data: this.track.id
};
this.$root.replaceQueue(res.data);
this.$root.playIndex(0);
} }
} }
} }

23
app/client/src/js/i18n.js Normal file
View File

@ -0,0 +1,23 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n);
function loadLocaleMessages () {
const locales = require.context('../locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages
}
export default new VueI18n({
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
messages: loadLocaleMessages()
})

View File

@ -8,6 +8,6 @@ Vue.use(Vuetify);
export default new Vuetify({ export default new Vuetify({
theme: { theme: {
dark: true dark: true,
} }
}); });

View File

@ -0,0 +1,112 @@
{
"Home": "القائمة الرئيسية",
"Browse": "تصفح",
"Library": "المكتبة",
"Tracks": "أغاني",
"Playlists": "قوائم تشغيل",
"Albums": "البومات",
"Artists": "فنانون",
"More": "المزيد",
"Settings": "الإعدادات",
"Downloads": "التنزيلات",
"Search or paste Deezer URL. Use / to quickly focus.": "ابحث أو الصق رابط ديزر, استخدم \"/\" للتركيز السريع.",
"Play": "تشغيل",
"Add to library": "إضافة إلى المكتبة",
"Download": "تنزيل",
"fans": "المتابِعين",
"tracks": "أغاني",
"Quality": "الجودة",
"Estimated size:": "الحجم المتوقع:",
"Start downloading": "بدء التنزيل",
"Cancel": "الغاء",
"Stream logging is disabled!": "تسجيل البث معطل!",
"Enable it in settings for history to work properly.": "فعله في الإعدادات لتفعيل تاريخ السماع بشكل صحيح.",
"History": "تاريخ السماع",
"Create new playlist": "انشاء قائمة تشغيل جديدة",
"TRACKS": "أغاني",
"Sort by": "ترتيب حسب",
"Date Added": "تاريخ الإضافة",
"Name (A-Z)": "الإسم (أ - ي)",
"Artist (A-Z)": "الفنان (أ - ي)",
"Album (A-Z)": "الألبوم (أ - ي)",
"Error loading lyrics or lyrics not found!": "خطأ في تحميل كلمات الاغنية او الكلمات غير موجودة!",
"Create playlist": "إنشاء قائمة التشغيل",
"Create": "إنشاء",
"Add to playlist": "اضافة الى قائمة التشغيل",
"Create new": "إنشاء جديد",
"Remove": "إزالة",
"Play next": "شغل التالي",
"Add to queue": "إضافة إلى قائمة الانتظار",
"Remove from library": "إزالة من المكتبة",
"Remove from playlist": "إزالة من قائمة التشغيل",
"Play track mix": "تشغيل مزيج الاغاني",
"Go to": "الذهاب الى",
"Track Mix": "مزيج الاغاني",
"Duration": "المدة",
"Released": "تم إصداره",
"Disk": "القرص",
"albums": "البومات",
"Play top": "تشغيل الأفضل",
"Radio": "راديو",
"Show all albums": "اضهار كل الالبومات",
"Show all singles": "إظهار كل الأغاني المنفردة",
"Show more": "اظهار المزيد",
"Downloaded": "تم التنزيل",
"Queue": "قائمة الانتظار",
"Total": "المجموع",
"Stop": "إيقاف",
"Start": "بدء",
"Show folder": "عرض المجلدات",
"Clear queue": "تفريغ قائمة الإنتظار",
"Playing from": "التشغيل من",
"Info": "معلومات",
"Lyrics": "كلمات الأغنية",
"Track number": "رقم الأغنية",
"Disk number": "رقم القرص",
"Explicit": "صريحة (شتم)",
"Source": "المصدر",
"ID": "الرقم التعريفي",
"Error logging in!": "خطأ في تسجيل الدخول!",
"Please try again later, or try another account.": "الرجاء المحاولة مرة أخرى لاحقا، أو حاول حساب آخر.",
"Logout": "تسجيل الخروج",
"Login using browser": "تسجيل الدخول باستخدام المتصفح",
"Please login using your Deezer account:": "يرجى تسجيل الدخول باستخدام حساب ديزر الخاص بك:",
"...or paste your ARL/Token below:": "...أو لصق ARL/الرمز الخاص بك أدناه:",
"ARL/Token": "ARL/الرمز المميز",
"Login": "تسجيل الدخول",
"By using this program, you disagree with Deezer's ToS.": "باستخدام هذا البرنامج، أنت لا توافق على شروط خدمة ديزر.",
"Only in Electron version!": "فقط في إصدار إلكترون!",
"Search results for:": "نتائج البحث عن:",
"Error loading data!": "خطأ في تحميل البيانات!",
"Try again later!": "حاول مرة اخرى لاحقا!",
"Search": "بحث",
"Streaming Quality": "جودة التشغيل",
"Download Quality": "جودة التنزيل",
"Downloads Directory": "مسار التنزيل",
"Simultaneous downloads": "عدد التحميلات في نفس الوقت",
"Always show download confirm dialog before downloading.": "اضهار مربع تأكيد التنزيل دائماً قبل التنزيل.",
"Show download dialog": "عرض مربع تأكيد التنزيل",
"Create folders for artists": "إنشاء ملفات للفنان",
"Create folders for albums": "إنشاء ملفات للالبوم",
"Download lyrics": "تنزيل ملف كلمات الاغنية. Lrc",
"Variables": "المتغيرات",
"UI": "واجهة المستخدم",
"Show autocomplete in search": "إظهار الإكمال التلقائي في البحث",
"Integrations": "الدمج",
"This allows listening history, flow and recommendations to work properly.": "وهذا يتيح لسجل الاستماع و فلو والتوصيات, العمل على نحو سليم.",
"Log track listens to Deezer": "سِجِل استماع الاغاني الى ديزر",
"Connect your LastFM account to allow scrobbling.": "قم بتوصيل حساب LastFM الخاص بك للسماح بالتسجيل.",
"Login with LastFM": "تسجيل الدخول في LastFM",
"Disconnect LastFM": "تسجيل الخروج من LastFm",
"Requires restart to apply!": "يتطلب إعادة التشغيل من أجل التطبيق!",
"Enable Discord Rich Presence, requires restart to toggle!": "تمكين فعالية دسكورد، يتطلب إعادة تشغيل للتبديل!",
"Discord Rich Presence": "فعالية دسكورد",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "تمكين زر انضمام دسكورد لمزامنة الاغاني، يتطلب إعادة تشغيل للتبديل!",
"Discord Join Button": "زر الانضمام في دسكورد",
"Other": "أخرى",
"Minimize to tray": "تصغير إلى شريط المهام",
"Don't minimize to tray": "عدم التصغير إلى شريط المهام",
"Close on exit": "إغلاق عند الخروج",
"Settings saved!": "تم حفظ الإعدادات!",
"Available only in Electron version!": "متاح فقط في اصدار الإلكترون!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Start",
"Browse": "Durchsuchen",
"Library": "Mediathek",
"Tracks": "Titel",
"Playlists": "Wiedergabelisten",
"Albums": "Alben",
"Artists": "Künstler",
"More": "Mehr",
"Settings": "Einstellungen",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Suche oder füge Deezer URL ein. Benutze \"/\" um schnell zu fokussieren.",
"Play": "Wiedergeben",
"Add to library": "Zur Mediathek hinzufügen",
"Download": "Download",
"fans": "Fans",
"tracks": "Titel",
"Quality": "Qualität",
"Estimated size:": "Geschätzte Zeit:",
"Start downloading": "Download beginnen",
"Cancel": "Abbrechen",
"Stream logging is disabled!": "Streamprotokollierung ist deaktiviert!",
"Enable it in settings for history to work properly.": "Aktiviere es in den Einstellungen, damit der Verlauf korrekt funktioniert.",
"History": "Verlauf",
"Create new playlist": "Neue Wiedergabeliste erstellen",
"TRACKS": "Titel",
"Sort by": "Sortieren nach",
"Date Added": "Hinzugefügt am",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Künstler (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Fehler beim Laden der Songtexte oder Songtexte nicht gefunden!",
"Create playlist": "Wiedergabeliste erstellen",
"Create": "Erstellen",
"Add to playlist": "Zur Wiedergabeliste hinzufügen",
"Create new": "Neu erstellen",
"Remove": "Entfernen",
"Play next": "Als nächstes spielen",
"Add to queue": "Zur Warteschleife hinzufügen",
"Remove from library": "Aus der Mediathek entfernen",
"Remove from playlist": "Aus Wiedergabeliste entfernen",
"Play track mix": "Track Mix abspielen",
"Go to": "Gehe zu",
"Track Mix": "Track Mix",
"Duration": "Dauer",
"Released": "Veröffentlicht",
"Disk": "Disk",
"albums": "Alben",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Zeige alle Alben",
"Show all singles": "Zeige alle Singles",
"Show more": "Mehr anzeigen",
"Downloaded": "Heruntergeladen",
"Queue": "Warteschleife",
"Total": "Gesamt",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Ordner anzeigen",
"Clear queue": "Warteschleife löschen",
"Playing from": "Wiedergabe von",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Titelnummer",
"Disk number": "Disk-Nummer",
"Explicit": "Explizit",
"Source": "Quelle",
"ID": "ID",
"Error logging in!": "Fehler beim einloggen!",
"Please try again later, or try another account.": "Bitte versuche es später noch einmal oder versuche es mit einem anderen Konto.",
"Logout": "Abmelden",
"Login using browser": "Anmeldung über Browser",
"Please login using your Deezer account:": "Bitte melde dich mit deinem Deezer-Konto an:",
"...or paste your ARL/Token below:": "...oder füge dein ARL/Token unten ein:",
"ARL/Token": "ARL/Token",
"Login": "Anmeldung",
"By using this program, you disagree with Deezer's ToS.": "Durch die Verwendung dieses Programms lehnen Sie Deezer's ToS ab.",
"Only in Electron version!": "Nur in der Electron-Version!",
"Search results for:": "Suchergebnisse für:",
"Error loading data!": "Fehler beim Laden der Daten!",
"Try again later!": "Versuch's später nochmal!",
"Search": "Suche",
"Streaming Quality": "Streamqualität",
"Download Quality": "Download-Qualität",
"Downloads Directory": "Downloadverzeichnis",
"Simultaneous downloads": "Gleichzeitige Downloads",
"Always show download confirm dialog before downloading.": "Downloadbestätigungsdialog immer vor dem Download anzeigen.",
"Show download dialog": "Download-Dialog anzeigen",
"Create folders for artists": "Ordner für Künstler erstellen",
"Create folders for albums": "Ordner für Alben erstellen",
"Download lyrics": "Download Lyrics",
"Variables": "Variablen",
"UI": "Benutzeroberfläche",
"Show autocomplete in search": "Auto-Vervollständigung in der Suche anzeigen",
"Integrations": "Integrationen",
"This allows listening history, flow and recommendations to work properly.": "Dies ermöglicht das korrekte Arbeiten von Wiedergabeverlauf, Flow und Empfehlungen.",
"Log track listens to Deezer": "Prokotolliere gehörte Titel auf Deezer",
"Connect your LastFM account to allow scrobbling.": "Verbinde dein LastFM-Konto, um das Scrobbing zu erlauben.",
"Login with LastFM": "Anmelden mit LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Erfordert einen Neustart der App!",
"Enable Discord Rich Presence, requires restart to toggle!": "Discord Rich Presence aktivieren, erfordert einen Neustart zum Umschalten!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Aktiviere Discord Join für die Synchronisierung von Titel, benötigt einen Neustart zum Umschalten!",
"Discord Join Button": "Discord Join Button",
"Other": "Andere",
"Minimize to tray": "In Statusleiste minimieren",
"Don't minimize to tray": "Nicht in Statusleiste minimieren",
"Close on exit": "Beim Beenden schließen",
"Settings saved!": "Einstellungen gespeichert!",
"Available only in Electron version!": "Nur in der Electron-Version verfügbar!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -0,0 +1,112 @@
{
"Home": "Home",
"Browse": "Browse",
"Library": "Library",
"Tracks": "Tracks",
"Playlists": "Playlists",
"Albums": "Albums",
"Artists": "Artists",
"More": "More",
"Settings": "Settings",
"Downloads": "Downloads",
"Search or paste Deezer URL. Use / to quickly focus.": "Search or paste Deezer URL. Use \"/\" to quickly focus.",
"Play": "Play",
"Add to library": "Add to library",
"Download": "Download",
"fans": "fans",
"tracks": "tracks",
"Quality": "Quality",
"Estimated size:": "Estimated size:",
"Start downloading": "Start downloading",
"Cancel": "Cancel",
"Stream logging is disabled!": "Stream logging is disabled!",
"Enable it in settings for history to work properly.": "Enable it in settings for history to work properly.",
"History": "History",
"Create new playlist": "Create new playlist",
"TRACKS": "TRACKS",
"Sort by": "Sort by",
"Date Added": "Date Added",
"Name (A-Z)": "Name (A-Z)",
"Artist (A-Z)": "Artist (A-Z)",
"Album (A-Z)": "Album (A-Z)",
"Error loading lyrics or lyrics not found!": "Error loading lyrics or lyrics not found!",
"Create playlist": "Create playlist",
"Create": "Create",
"Add to playlist": "Add to playlist",
"Create new": "Create new",
"Remove": "Remove",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Remove from library": "Remove from library",
"Remove from playlist": "Remove from playlist",
"Play track mix": "Play track mix",
"Go to": "Go to",
"Track Mix": "Track Mix",
"Duration": "Duration",
"Released": "Released",
"Disk": "Disk",
"albums": "albums",
"Play top": "Play top",
"Radio": "Radio",
"Show all albums": "Show all albums",
"Show all singles": "Show all singles",
"Show more": "Show more",
"Downloaded": "Downloaded",
"Queue": "Queue",
"Total": "Total",
"Stop": "Stop",
"Start": "Start",
"Show folder": "Show folder",
"Clear queue": "Clear queue",
"Playing from": "Playing from",
"Info": "Info",
"Lyrics": "Lyrics",
"Track number": "Track number",
"Disk number": "Disk number",
"Explicit": "Explicit",
"Source": "Source",
"ID": "ID",
"Error logging in!": "Error logging in!",
"Please try again later, or try another account.": "Please try again later, or try another account.",
"Logout": "Logout",
"Login using browser": "Login using browser",
"Please login using your Deezer account:": "Please login using your Deezer account:",
"...or paste your ARL/Token below:": "...or paste your ARL/Token below:",
"ARL/Token": "ARL/Token",
"Login": "Login",
"By using this program, you disagree with Deezer's ToS.": "By using this program, you disagree with Deezer's ToS.",
"Only in Electron version!": "Only in Electron version!",
"Search results for:": "Search results for:",
"Error loading data!": "Error loading data!",
"Try again later!": "Try again later!",
"Search": "Search",
"Streaming Quality": "Streaming Quality",
"Download Quality": "Download Quality",
"Downloads Directory": "Downloads Directory",
"Simultaneous downloads": "Simultaneous downloads",
"Always show download confirm dialog before downloading.": "Always show download confirm dialog before downloading.",
"Show download dialog": "Show download dialog",
"Create folders for artists": "Create folders for artists",
"Create folders for albums": "Create folders for albums",
"Download lyrics": "Download lyrics",
"Variables": "Variables",
"UI": "UI",
"Show autocomplete in search": "Show autocomplete in search",
"Integrations": "Integrations",
"This allows listening history, flow and recommendations to work properly.": "This allows listening history, flow and recommendations to work properly.",
"Log track listens to Deezer": "Log track listens to Deezer",
"Connect your LastFM account to allow scrobbling.": "Connect your LastFM account to allow scrobbling.",
"Login with LastFM": "Login with LastFM",
"Disconnect LastFM": "Disconnect LastFM",
"Requires restart to apply!": "Requires restart to apply!",
"Enable Discord Rich Presence, requires restart to toggle!": "Enable Discord Rich Presence, requires restart to toggle!",
"Discord Rich Presence": "Discord Rich Presence",
"Enable Discord join button for syncing tracks, requires restart to toggle!": "Enable Discord join button for syncing tracks, requires restart to toggle!",
"Discord Join Button": "Discord Join Button",
"Other": "Other",
"Minimize to tray": "Minimize to tray",
"Don't minimize to tray": "Don't minimize to tray",
"Close on exit": "Close on exit",
"Settings saved!": "Settings saved!",
"Available only in Electron version!": "Available only in Electron version!"
}

View File

@ -5,6 +5,7 @@ import vuetify from './js/vuetify';
import axios from 'axios'; import axios from 'axios';
import VueEsc from 'vue-esc'; import VueEsc from 'vue-esc';
import VueSocketIO from 'vue-socket.io'; import VueSocketIO from 'vue-socket.io';
import i18n from './js/i18n';
//Globals //Globals
let ipcRenderer; let ipcRenderer;
@ -52,7 +53,8 @@ Vue.prototype.$filesize = (bytes) => {
//Sockets //Sockets
Vue.use(new VueSocketIO({ Vue.use(new VueSocketIO({
connection: window.location.origin connection: window.location.toString(),
options: {path: '/socket'}
})); }));
Vue.config.productionTip = false; Vue.config.productionTip = false;
@ -66,10 +68,7 @@ new Vue({
authorized: false, authorized: false,
loadingPromise: null, loadingPromise: null,
//Downloads downloads: {},
downloading: false,
downloads: [],
download: null,
//Player //Player
track: null, track: null,
@ -111,6 +110,7 @@ new Vue({
//Used to prevent double listen logging //Used to prevent double listen logging
logListenId: null logListenId: null
}, },
methods: { methods: {
// PLAYBACK METHODS // PLAYBACK METHODS
isPlaying() { isPlaying() {
@ -199,7 +199,12 @@ new Vue({
if (this.audio) this.audio.currentTime = 0; if (this.audio) this.audio.currentTime = 0;
//Load track meta //Load track meta
this.playbackInfo = await this.loadPlaybackInfo(track.streamUrl, track.duration); let playbackInfo = await this.loadPlaybackInfo(track.streamUrl, track.duration);
if (!playbackInfo) {
this.skipNext();
return;
}
this.playbackInfo = playbackInfo;
//Stream URL //Stream URL
let url = `${window.location.origin}${this.playbackInfo.url}`; let url = `${window.location.origin}${this.playbackInfo.url}`;
@ -290,6 +295,7 @@ new Vue({
//Update media session with current track metadata //Update media session with current track metadata
updateMediaSession() { updateMediaSession() {
if (!this.track || !('mediaSession' in navigator)) return; if (!this.track || !('mediaSession' in navigator)) return;
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
title: this.track.title, title: this.track.title,
@ -315,11 +321,25 @@ new Vue({
//Get playback info //Get playback info
let quality = this.settings.streamQuality; let quality = this.settings.streamQuality;
let infoUrl = `/streaminfo/${streamUrl}?q=${quality}`; let infoUrl = `/streaminfo/${streamUrl}?q=${quality}`;
let res = await this.$axios.get(infoUrl); let res;
try {
res = await this.$axios.get(infoUrl);
} catch (_) {
return null;
}
let info = res.data; let info = res.data;
//Calculate flac bitrate //Generate qualityString
if (!info.quality.includes('kbps')) { switch (info.quality) {
info.quality = Math.round((parseInt(info.quality, 10)*8) / duration) + 'kbps'; case 9:
info.qualityString = 'FLAC ' + Math.round((info.size*8) / duration) + 'kbps';
break;
case 3:
info.qualityString = 'MP3 320kbps';
break;
case 1:
info.qualityString = 'MP3 128kbps';
break;
} }
return info; return info;
}, },
@ -341,6 +361,10 @@ new Vue({
//Load meta //Load meta
this.gapless.track = this.queue.data[this.queue.index + 1]; this.gapless.track = this.queue.data[this.queue.index + 1];
let info = await this.loadPlaybackInfo(this.gapless.track.streamUrl, this.gapless.track.duration); let info = await this.loadPlaybackInfo(this.gapless.track.streamUrl, this.gapless.track.duration);
if (!info) {
this.resetGapless();
if (this.gapless.promise) resolve();
}
this.gapless.info = info this.gapless.info = info
this.gapless.audio = new Audio(`${window.location.origin}${info.url}`); this.gapless.audio = new Audio(`${window.location.origin}${info.url}`);
@ -382,12 +406,12 @@ new Vue({
//Get downloads from server //Get downloads from server
async getDownloads() { async getDownloads() {
let res = await this.$axios.get('/downloads'); let res = await this.$axios.get('/downloads');
this.downloading = res.data.downloading; if (res.data)
this.downloads = res.data.downloads; this.downloads = res.data;
}, },
//Start stop downloading //Start stop downloading
async toggleDownload() { async toggleDownload() {
if (this.downloading) { if (this.downloads.downloading) {
await this.$axios.delete('/download'); await this.$axios.delete('/download');
} else { } else {
await this.$axios.put('/download'); await this.$axios.put('/download');
@ -411,8 +435,12 @@ new Vue({
//Send state update to integrations //Send state update to integrations
async updateState() { async updateState() {
//Wait for duration //Wait for duration
if (this.state == 2 && (this.duration() == null || isNaN(this.duration()))) if (this.state == 2 && (this.duration() == null || isNaN(this.duration()))) {
await new Promise((res) => setTimeout(res, 1000)); setTimeout(() => {
this.updateState();
}, 500);
return;
}
this.$socket.emit('stateChange', { this.$socket.emit('stateChange', {
position: this.position, position: this.position,
duration: this.duration(), duration: this.duration(),
@ -424,14 +452,20 @@ new Vue({
if (this.settings.electron) { if (this.settings.electron) {
ipcRenderer.send('playing', this.state == 2); ipcRenderer.send('playing', this.state == 2);
} }
},
updateLanguage(l) {
i18n.locale = l;
} }
}, },
async created() { async created() {
//Load settings, create promise so `/login` can await it //Load settings, create promise so `/login` can await it
let r; let r;
this.loadingPromise = new Promise((resolve) => r = resolve); this.loadingPromise = new Promise((resolve) => r = resolve);
let res = await this.$axios.get('/settings'); let res = await this.$axios.get('/settings');
this.settings = res.data; this.settings = res.data;
this.$vuetify.theme.themes.dark.primary = this.settings.primaryColor;
i18n.locale = this.settings.language;
this.volume = this.settings.volume; this.volume = this.settings.volume;
//Restore playback data //Restore playback data
@ -480,17 +514,17 @@ new Vue({
} }
//Get downloads //Get downloads
this.getDownloads(); await this.getDownloads();
//Sockets //Sockets
//Queue change //Queue change
this.sockets.subscribe('downloads', (data) => { this.sockets.subscribe('downloads', (data) => {
this.downloading = data.downloading; this.downloads = data;
this.downloads = data.downloads;
}); });
//Current download change //Current download change
this.sockets.subscribe('download', (data) => { this.sockets.subscribe('currentlyDownloading', (data) => {
this.download = data; this.downloads.threads = data;
}); });
//Play at offset (for integrations) //Play at offset (for integrations)
this.sockets.subscribe('playOffset', async (data) => { this.sockets.subscribe('playOffset', async (data) => {
@ -501,6 +535,7 @@ new Vue({
r(); r();
}, },
mounted() { mounted() {
//Save settings on unload //Save settings on unload
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
@ -548,6 +583,7 @@ new Vue({
} }
}); });
}, },
watch: { watch: {
//Watch state for integrations //Watch state for integrations
state() { state() {
@ -558,5 +594,6 @@ new Vue({
router, router,
vuetify, vuetify,
i18n,
render: function (h) { return h(App) } render: function (h) { return h(App) }
}).$mount('#app'); }).$mount('#app');

View File

@ -16,24 +16,24 @@
<h1>{{album.title}}</h1> <h1>{{album.title}}</h1>
<h3>{{album.artistString}}</h3> <h3>{{album.artistString}}</h3>
<div class='mt-2' v-if='!loading'> <div class='mt-2' v-if='!loading'>
<span class='text-subtitle-2'>{{album.tracks.length}} tracks</span><br> <span class='text-subtitle-2'>{{album.tracks.length}} {{$t("tracks")}}</span><br>
<span class='text-subtitle-2'>Duration: {{duration}}</span><br> <span class='text-subtitle-2'>{{$t("Duration")}}: {{duration}}</span><br>
<span class='text-subtitle-2'>{{$numberString(album.fans)}} fans</span><br> <span class='text-subtitle-2'>{{$numberString(album.fans)}} fans</span><br>
<span class='text-subtitle-2'>Released: {{album.releaseDate}}</span><br> <span class='text-subtitle-2'>{{$t("Released")}}: {{album.releaseDate}}</span><br>
</div> </div>
<div class='my-2'> <div class='my-2'>
<v-btn color='primary' class='mx-1' @click='play'> <v-btn color='primary' class='mx-1' @click='play'>
<v-icon left>mdi-play</v-icon> <v-icon left>mdi-play</v-icon>
Play {{$t("Play")}}
</v-btn> </v-btn>
<v-btn color='red' class='mx-1' @click='library' :loading='libraryLoading'> <v-btn color='red' class='mx-1' @click='library' :loading='libraryLoading'>
<v-icon left>mdi-heart</v-icon> <v-icon left>mdi-heart</v-icon>
Library {{$t("Library")}}
</v-btn> </v-btn>
<v-btn color='green' class='mx-1' @click='download'> <v-btn color='green' class='mx-1' @click='download'>
<v-icon left>mdi-download</v-icon> <v-icon left>mdi-download</v-icon>
Download {{$t("Download")}}
</v-btn> </v-btn>
</div> </div>
</div> </div>
@ -48,7 +48,7 @@
v-if='index == 0 || track.diskNumber != album.tracks[index-1].diskNumber' v-if='index == 0 || track.diskNumber != album.tracks[index-1].diskNumber'
class='mx-4 text-subtitle-1' class='mx-4 text-subtitle-1'
> >
Disk {{track.diskNumber}} {{$t("Disk")}} {{track.diskNumber}}
</div> </div>
<TrackTile :track='track' @click='playTrack(index)'></TrackTile> <TrackTile :track='track' @click='playTrack(index)'></TrackTile>

View File

@ -16,18 +16,22 @@
</v-overlay> </v-overlay>
<h1>{{artist.name}}</h1> <h1>{{artist.name}}</h1>
<div class='mt-2' v-if='!loading'> <div class='mt-2' v-if='!loading'>
<span class='text-subtitle-2'>{{artist.albumCount}} albums</span><br> <span class='text-subtitle-2'>{{artist.albumCount}} {{$t("albums")}}</span><br>
<span class='text-subtitle-2'>{{$numberString(artist.fans)}} fans</span><br> <span class='text-subtitle-2'>{{$numberString(artist.fans)}} {{$t("fans")}}</span><br>
</div> </div>
<div class='my-2'> <div class='my-2'>
<v-btn color='primary' class='mx-1' @click='play'> <v-btn color='primary' class='mx-1' @click='play'>
<v-icon left>mdi-play</v-icon> <v-icon left>mdi-play</v-icon>
Play top {{$t("Play top")}}
</v-btn> </v-btn>
<v-btn color='red' class='mx-1' @click='library' :loading='libraryLoading'> <v-btn color='red' class='mx-1' @click='library' :loading='libraryLoading'>
<v-icon left>mdi-heart</v-icon> <v-icon left>mdi-heart</v-icon>
Library {{$t("Library")}}
</v-btn>
<v-btn color='green' class='mx-1' @click='radio' v-if='artist.radio'>
<v-icon left>mdi-radio</v-icon>
{{$t("Radio")}}
</v-btn> </v-btn>
</div> </div>
</div> </div>
@ -67,7 +71,7 @@
<!-- Show all albums --> <!-- Show all albums -->
<v-list-item v-if='!allAlbums && index == 3' @click='allAlbums = true'> <v-list-item v-if='!allAlbums && index == 3' @click='allAlbums = true'>
<v-list-item-title>Show all albums</v-list-item-title> <v-list-item-title>{{$t("Show all albums")}}</v-list-item-title>
</v-list-item> </v-list-item>
</div> </div>
@ -91,7 +95,7 @@
<!-- Show all albums --> <!-- Show all albums -->
<v-list-item v-if='!allSingles && index == 3' @click='showAllSingles'> <v-list-item v-if='!allSingles && index == 3' @click='showAllSingles'>
<v-list-item-title>Show all singles</v-list-item-title> <v-list-item-title>{{$t('Show all singles')}}</v-list-item-title>
</v-list-item> </v-list-item>
</div> </div>
@ -179,6 +183,19 @@ export default {
this.allSingles = true; this.allSingles = true;
this.loadMoreAlbums(); this.loadMoreAlbums();
}, },
async radio() {
//Load
let res = await this.$axios.get('/smartradio/' + this.artist.id);
if (res.data) {
this.$root.queue.source = {
text: this.artist.name,
source: 'radio',
data: this.artist.id
};
this.$root.replaceQueue(res.data);
this.$root.playIndex(0);
}
},
//On scroll load more albums //On scroll load more albums
scroll(event) { scroll(event) {
if (!this.allAlbums && !this.allSingles) return; if (!this.allAlbums && !this.allSingles) return;

View File

@ -23,7 +23,7 @@
</div> </div>
<div v-if='section.hasMore' class='mx-2 align-center justify-center d-flex'> <div v-if='section.hasMore' class='mx-2 align-center justify-center d-flex'>
<v-btn @click='showMore(section)' color='primary'> <v-btn @click='showMore(section)' color='primary'>
Show more {{$t("Show more")}}
</v-btn> </v-btn>
</div> </div>
</div> </div>

View File

@ -1,51 +1,51 @@
<template> <template>
<div> <div>
<h1 class='pb-2'>Downloads</h1> <h1 class='pb-2'>{{$t("Downloads")}}</h1>
<div v-if='$root.downloads.downloading'>
<v-card v-if='$root.download' max-width='100%'> <v-card v-for='(download, index) in $root.downloads.threads' :key='"t" + index.toString()' max-width='100%'>
<v-list-item>
<v-list-item three-line> <v-list-item-avatar>
<v-list-item-avatar> <v-img :src='download.track.albumArt.thumb'></v-img>
<v-img :src='$root.download.track.albumArt.thumb'></v-img> </v-list-item-avatar>
</v-list-item-avatar> <v-list-item-content>
<v-list-item-content> <v-list-item-title>{{download.track.title}}</v-list-item-title>
<v-list-item-title>{{$root.download.track.title}}</v-list-item-title> <v-list-item-subtitle>
<v-list-item-subtitle> {{$t('Downloaded')}}: {{$filesize(download.downloaded)}} / {{$filesize(download.size)}}<br>
Downloaded: {{$filesize($root.download.downloaded)}} / {{$filesize($root.download.size)}}<br> </v-list-item-subtitle>
Path: {{$root.download.path}} </v-list-item-content>
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item> </v-list-item>
</v-card>
<h1 class='pb-2'>Queue:</h1> </v-card>
<div class='text-h6 mr-4 pb-2 d-flex'>Total: {{$root.downloads.length}} </div>
<h1 class='pb-2'>{{$t("Queue")}}:</h1>
<div class='text-h6 mr-4 pb-2 d-flex'>{{$t("Total")}}: {{$root.downloads.queue.length}}
<v-btn @click='$root.toggleDownload' class='ml-4' color='primary'> <v-btn @click='$root.toggleDownload' class='ml-4' color='primary'>
<div v-if='$root.downloading'> <div v-if='$root.downloads.downloading'>
<v-icon>mdi-stop</v-icon> <v-icon>mdi-stop</v-icon>
Stop {{$t("Stop")}}
</div> </div>
<div v-if='!$root.downloading'> <div v-if='!$root.downloads.downloading'>
<v-icon>mdi-download</v-icon> <v-icon>mdi-download</v-icon>
Start {{$t("Start")}}
</div> </div>
</v-btn> </v-btn>
<!-- Open dir --> <!-- Open dir -->
<v-btn @click='openDir' class='ml-4' v-if='$root.settings.electron'> <v-btn @click='openDir' class='ml-4' v-if='$root.settings.electron'>
<v-icon>mdi-folder</v-icon> <v-icon>mdi-folder</v-icon>
Show folder {{$t("Show folder")}}
</v-btn> </v-btn>
<!-- Delete all --> <!-- Delete all -->
<v-btn @click='deleteDownload(-1)' class='ml-4' color='red'> <v-btn @click='deleteDownload(-1)' class='ml-4' color='red'>
<v-icon>mdi-delete</v-icon> <v-icon>mdi-delete</v-icon>
Clear queue {{$t("Clear queue")}}
</v-btn> </v-btn>
</div> </div>
<!-- Downloads --> <!-- Queue -->
<v-list dense> <v-list dense>
<div v-for='(download, index) in $root.downloads' :key='download.id'> <div v-for='(download, index) in $root.downloads.queue' :key='download.id'>
<v-list-item dense> <v-list-item dense>
<v-list-item-avatar> <v-list-item-avatar>
<v-img :src='download.track.albumArt.thumb'></v-img> <v-img :src='download.track.albumArt.thumb'></v-img>

View File

@ -5,7 +5,7 @@
<v-btn icon @click='close'> <v-btn icon @click='close'>
<v-icon>mdi-close</v-icon> <v-icon>mdi-close</v-icon>
</v-btn> </v-btn>
<v-toolbar-title>Playing from: {{$root.queue.source.text}}</v-toolbar-title> <v-toolbar-title>{{$t("Playing from")}}: {{$root.queue.source.text}}</v-toolbar-title>
</v-app-bar> </v-app-bar>
<!-- Split to half --> <!-- Split to half -->
@ -122,13 +122,13 @@
<v-col class='col-6 pt-4'> <v-col class='col-6 pt-4'>
<v-tabs v-model='tab'> <v-tabs v-model='tab'>
<v-tab key='queue'> <v-tab key='queue'>
Queue {{$t("Queue")}}
</v-tab> </v-tab>
<v-tab key='info'> <v-tab key='info'>
Info {{$t("Info")}}
</v-tab> </v-tab>
<v-tab key='lyrics'> <v-tab key='lyrics'>
Lyrics {{$t("Lyrics")}}
</v-tab> </v-tab>
</v-tabs> </v-tabs>
@ -172,14 +172,13 @@
></ArtistTile> ></ArtistTile>
</v-list> </v-list>
<!-- Meta --> <!-- Meta -->
<h3>Duration: <span>{{$duration($root.track.duration)}}</span></h3> <h3>{{$t("Duration")}}: <span>{{$duration($root.track.duration)}}</span></h3>
<h3>Track number: {{$root.track.trackNumber}}</h3> <h3>{{$t("Track number")}}: {{$root.track.trackNumber}}</h3>
<h3>Disk number: {{$root.track.diskNumber}}</h3> <h3>{{$t("Disk number")}}: {{$root.track.diskNumber}}</h3>
<h3>Explicit: {{$root.track.explicit?"Yes":"No"}}</h3> <h3>{{$t("Explicit")}}: {{$root.track.explicit?"Yes":"No"}}</h3>
<h3>Source: {{$root.playbackInfo.source}}</h3> <h3>{{$t("Source")}}: {{$root.playbackInfo.source}}</h3>
<h3>Format: {{$root.playbackInfo.format}}</h3> <h3>{{$t("Quality")}}: {{$root.playbackInfo.qualityString}}</h3>
<h3>Quality: {{$root.playbackInfo.quality}}</h3> <h3>{{$t("ID")}}: {{$root.track.id}}</h3>
<h3>ID: {{$root.track.id}}</h3>
</v-list> </v-list>
</v-tab-item> </v-tab-item>
<!-- Lyrics --> <!-- Lyrics -->
@ -261,7 +260,7 @@ export default {
let offsetp = (v.pageX - seeker.$el.offsetLeft) / seeker.$el.clientWidth; let offsetp = (v.pageX - seeker.$el.offsetLeft) / seeker.$el.clientWidth;
let pos = offsetp * this.$root.duration(); let pos = offsetp * this.$root.duration();
this.$root.seek(pos); this.$root.seek(pos);
this.position = pos; this.position = pos / 1000;
this.seeking = false; this.seeking = false;
}, },
//Add/Remove track from library //Add/Remove track from library
@ -305,7 +304,9 @@ export default {
this.inLibrary = this.$root.libraryTracks.includes(this.$root.track.id); this.inLibrary = this.$root.libraryTracks.includes(this.$root.track.id);
}, },
'$root.position'() { '$root.position'() {
if (!this.seeking) this.position = this.$root.position / 1000; if (!this.seeking) {
this.position = this.$root.position / 1000;
}
}, },
} }
}; };

View File

@ -1,29 +1,29 @@
<template> <template>
<div> <div>
<h1>Library</h1> <h1>{{$t("Library")}}</h1>
<v-tabs v-model='tab'> <v-tabs v-model='tab'>
<v-tab key='tracks'> <v-tab key='tracks'>
Tracks {{$t("Tracks")}}
</v-tab> </v-tab>
<v-tab key='albums'> <v-tab key='albums'>
Albums {{$t("Albums")}}
</v-tab> </v-tab>
<v-tab key='artists'> <v-tab key='artists'>
Artists {{$t("Artists")}}
</v-tab> </v-tab>
<v-tab key='playlists'> <v-tab key='playlists'>
Playlists {{$t("Playlists")}}
</v-tab> </v-tab>
<v-tab key='history'> <v-tab key='history'>
History {{$t("History")}}
</v-tab> </v-tab>
</v-tabs> </v-tabs>
<v-tabs-items v-model='tab'> <v-tabs-items v-model='tab'>
<!-- Tracks --> <!-- Tracks -->
<v-tab-item key='tracks'> <v-tab-item key='tracks'>
<LibraryTracks height='calc(100vh - 240px)'></LibraryTracks> <LibraryTracks height='calc(100vh - 290px)'></LibraryTracks>
</v-tab-item> </v-tab-item>
<!-- Albums --> <!-- Albums -->

View File

@ -9,11 +9,11 @@
<!-- Error --> <!-- Error -->
<v-card class='text-center pa-4' v-if='error'> <v-card class='text-center pa-4' v-if='error'>
<h1 class='text--red'>Error logging in!</h1> <h1 class='text--red'>{{$t("Error logging in!")}}</h1>
<h3>Please try again later, or try another account.</h3> <h3>{{$t("Please try again later, or try another account.")}}</h3>
<v-btn large class='my-4' @click='logout'> <v-btn large class='my-4' @click='logout'>
<v-icon left>mdi-logout-variant</v-icon> <v-icon left>mdi-logout-variant</v-icon>
Logout {{$t("Logout")}}
</v-btn> </v-btn>
</v-card> </v-card>
@ -21,23 +21,24 @@
<div v-if='showForm' class='text-center'> <div v-if='showForm' class='text-center'>
<v-img src='banner.png' contain max-width='400px' class='py-8'></v-img> <v-img src='banner.png' contain max-width='400px' class='py-8'></v-img>
<h3>Please login using your Deezer account:</h3> <h3>{{$t("Please login using your Deezer account:")}}</h3>
<v-btn large class='my-2 mb-4 primary' @click='browserLogin'> <v-btn large class='my-2 mb-4 primary' @click='browserLogin'>
<v-icon left>mdi-open-in-app</v-icon> <v-icon left>mdi-open-in-app</v-icon>
Login using browser {{$t("Login using browser")}}
</v-btn> </v-btn>
<h3 class='mt-4'>...or paste your ARL/Token below:</h3> <h3 class='mt-4'>{{$t("...or paste your ARL/Token below:")}}</h3>
<v-text-field label='ARL/Token' v-model='arl'> <v-text-field :label='$t("ARL/Token")' v-model='arl'>
</v-text-field> </v-text-field>
<v-btn large class='my-4 primary' :loading='authorizing' @click='login'> <v-btn large class='my-4 primary' :loading='authorizing' @click='login'>
<v-icon left>mdi-login-variant</v-icon>Login <v-icon left>mdi-login-variant</v-icon>
{{$t("Login")}}
</v-btn> </v-btn>
<br> <br>
<span class='mt-8 text-caption'> <span class='mt-8 text-caption'>
By using this program, you disagree with Deezer's ToS. {{$t("By using this program, you disagree with Deezer's ToS.")}}
</span> </span>
</div> </div>
@ -99,7 +100,7 @@ export default {
}, },
//Login using browser //Login using browser
browserLogin() { browserLogin() {
if (!this.$root.settings.electron) return alert('Only in Electron version!'); if (!this.$root.settings.electron) return alert(this.$t('Only in Electron version!'));
const {ipcRenderer} = window.require('electron'); const {ipcRenderer} = window.require('electron');
ipcRenderer.on('browserLogin', (event, newArl) => { ipcRenderer.on('browserLogin', (event, newArl) => {

View File

@ -17,23 +17,23 @@
<h3>{{playlist.user.name}}</h3> <h3>{{playlist.user.name}}</h3>
<h5>{{playlist.description}}</h5> <h5>{{playlist.description}}</h5>
<div class='mt-2' v-if='!loading'> <div class='mt-2' v-if='!loading'>
<span class='text-subtitle-2'>{{playlist.trackCount}} tracks</span><br> <span class='text-subtitle-2'>{{playlist.trackCount}} {{$t("tracks")}}</span><br>
<span class='text-subtitle-2'>Duration: {{$duration(playlist.duration)}}</span><br> <span class='text-subtitle-2'>{{$t("Duration")}}: {{$duration(playlist.duration)}}</span><br>
<span class='text-subtitle-2'>{{$numberString(playlist.fans)}} fans</span><br> <span class='text-subtitle-2'>{{$numberString(playlist.fans)}} {{$t('fans')}}</span><br>
</div> </div>
<div class='my-1'> <div class='my-1'>
<v-btn color='primary' class='mr-1' @click='play'> <v-btn color='primary' class='mr-1' @click='play'>
<v-icon left>mdi-play</v-icon> <v-icon left>mdi-play</v-icon>
Play {{$t('Play')}}
</v-btn> </v-btn>
<v-btn color='red' class='mx-1' @click='library' :loading='libraryLoading'> <v-btn color='red' class='mx-1' @click='library' :loading='libraryLoading'>
<v-icon left>mdi-heart</v-icon> <v-icon left>mdi-heart</v-icon>
Library {{$t('Library')}}
</v-btn> </v-btn>
<v-btn color='green' class='mx-1' @click='download'> <v-btn color='green' class='mx-1' @click='download'>
<v-icon left>mdi-download</v-icon> <v-icon left>mdi-download</v-icon>
Download {{$t('Download')}}
</v-btn> </v-btn>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<h1 class='pb-2'>Search results for: "{{query}}"</h1> <h1 class='pb-2'>{{$t('Search results for:')}} "{{query}}"</h1>
<!-- Loading overlay --> <!-- Loading overlay -->
<v-overlay opacity='0.9' :value='loading' z-index='3'> <v-overlay opacity='0.9' :value='loading' z-index='3'>
@ -12,24 +12,24 @@
<!-- Error overlay --> <!-- Error overlay -->
<v-overlay opacity='0.9' :value='error' z-index="3"> <v-overlay opacity='0.9' :value='error' z-index="3">
<h1 class='red--text'>Error loading data!</h1><br> <h1 class='red--text'>{{$t("Error loading data!")}}</h1><br>
<h3>Try again later!</h3> <h3>{{$t("Try again later!")}}</h3>
</v-overlay> </v-overlay>
<!-- Tabs --> <!-- Tabs -->
<v-tabs v-model="tab"> <v-tabs v-model="tab">
<v-tabs-slider></v-tabs-slider> <v-tabs-slider></v-tabs-slider>
<v-tab key="tracks"> <v-tab key="tracks">
<v-icon left>mdi-music-note</v-icon>Tracks <v-icon left>mdi-music-note</v-icon>{{$t("Tracks")}}
</v-tab> </v-tab>
<v-tab> <v-tab>
<v-icon left>mdi-album</v-icon>Albums <v-icon left>mdi-album</v-icon>{{$t("Albums")}}
</v-tab> </v-tab>
<v-tab> <v-tab>
<v-icon left>mdi-account-music</v-icon>Artists <v-icon left>mdi-account-music</v-icon>{{$t("Artists")}}
</v-tab> </v-tab>
<v-tab> <v-tab>
<v-icon left>mdi-playlist-music</v-icon>Playlists <v-icon left>mdi-playlist-music</v-icon>{{$t("Playlists")}}
</v-tab> </v-tab>
</v-tabs> </v-tabs>
@ -130,7 +130,7 @@ export default {
//On click for track tile //On click for track tile
playTrack(i) { playTrack(i) {
this.$root.queue.source = { this.$root.queue.source = {
text: "Search", text: this.$t("Search"),
source: "search", source: "search",
data: this.query data: this.query
}; };

View File

@ -1,10 +1,10 @@
<template> <template>
<div> <div>
<h1 class='pb-2'>Settings</h1> <h1 class='pb-2'>{{$t('Settings')}}</h1>
<v-list> <v-list>
<v-select <v-select
class='px-4' class='px-4'
label='Streaming Quality' :label='$t("Streaming Quality")'
persistent-hint persistent-hint
:items='qualities' :items='qualities'
@change='updateStreamingQuality' @change='updateStreamingQuality'
@ -13,7 +13,7 @@
<v-select <v-select
class='px-4' class='px-4'
label='Download Quality' :label='$t("Download Quality")'
persistent-hint persistent-hint
:items='qualities' :items='qualities'
@change='updateDownloadQuality' @change='updateDownloadQuality'
@ -23,20 +23,32 @@
<!-- Download path --> <!-- Download path -->
<v-text-field <v-text-field
class='px-4' class='px-4'
label='Downloads Directory' :label='$t("Downloads Directory")'
v-model='$root.settings.downloadsPath' v-model='$root.settings.downloadsPath'
append-icon='mdi-open-in-app' append-icon='mdi-open-in-app'
@click:append='selectDownloadPath' @click:append='selectDownloadPath'
></v-text-field> ></v-text-field>
<!-- Download threads -->
<v-slider
:label='$t("Simultaneous downloads")'
min='1'
max='16'
thumb-label
step='1'
ticks
class='px-4'
v-model='$root.settings.downloadThreads'
></v-slider>
<!-- Download dialog --> <!-- Download dialog -->
<v-list-item> <v-list-item>
<v-list-item-action> <v-list-item-action>
<v-checkbox v-model='$root.settings.downloadDialog'></v-checkbox> <v-checkbox v-model='$root.settings.downloadDialog'></v-checkbox>
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Show download dialog</v-list-item-title> <v-list-item-title>{{$t("Show download dialog")}}</v-list-item-title>
<v-list-item-subtitle>Always show download confirm dialog before downloading.</v-list-item-subtitle> <v-list-item-subtitle>{{$t("Always show download confirm dialog before downloading.")}}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@ -46,7 +58,7 @@
<v-checkbox v-model='$root.settings.createArtistFolder'></v-checkbox> <v-checkbox v-model='$root.settings.createArtistFolder'></v-checkbox>
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Create folders for artists</v-list-item-title> <v-list-item-title>{{$t("Create folders for artists")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Create album folder --> <!-- Create album folder -->
@ -55,9 +67,19 @@
<v-checkbox v-model='$root.settings.createAlbumFolder'></v-checkbox> <v-checkbox v-model='$root.settings.createAlbumFolder'></v-checkbox>
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Create folders for albums</v-list-item-title> <v-list-item-title>{{$t("Create folders for albums")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Download lyrics -->
<v-list-item>
<v-list-item-action>
<v-checkbox v-model='$root.settings.downloadLyrics'></v-checkbox>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>{{$t("Download lyrics")}}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Download naming --> <!-- Download naming -->
<v-text-field <v-text-field
@ -65,11 +87,34 @@
label='Download Filename' label='Download Filename'
persistent-hint persistent-hint
v-model='$root.settings.downloadFilename' v-model='$root.settings.downloadFilename'
hint='Variables: %title%, %artists%, %artist%, %feats%, %trackNumber%, %0trackNumber%, %album%' :hint='$t("Variables") + ": %title%, %artists%, %artist%, %feats%, %trackNumber%, %0trackNumber%, %album%, %year%"'
></v-text-field> ></v-text-field>
<!-- UI -->
<v-subheader>{{$t("UI")}}</v-subheader>
<v-divider></v-divider>
<!-- Language -->
<v-select
class='mt-2 px-4'
label='Language'
persistent-hint
:items='languageNames'
@change='updateLanguage'
></v-select>
<!-- Autocomplete -->
<v-list-item>
<v-list-item-action>
<v-checkbox v-model='$root.settings.showAutocomplete'></v-checkbox>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>{{$t("Show autocomplete in search")}}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<!-- Accounts --> <!-- Accounts -->
<v-subheader>Integrations</v-subheader> <v-subheader>{{$t("Integrations")}}</v-subheader>
<v-divider></v-divider> <v-divider></v-divider>
<!-- Log listening --> <!-- Log listening -->
@ -78,8 +123,8 @@
<v-checkbox v-model='$root.settings.logListen'></v-checkbox> <v-checkbox v-model='$root.settings.logListen'></v-checkbox>
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Log track listens to Deezer</v-list-item-title> <v-list-item-title>{{$t("Log track listens to Deezer")}}</v-list-item-title>
<v-list-item-subtitle>This allows listening history, flow and recommendations to work properly.</v-list-item-subtitle> <v-list-item-subtitle>{{$t("This allows listening history, flow and recommendations to work properly.")}}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- LastFM --> <!-- LastFM -->
@ -88,8 +133,8 @@
<v-img src='lastfm.svg'></v-img> <v-img src='lastfm.svg'></v-img>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Login with LastFM</v-list-item-title> <v-list-item-title>{{$t("Login with LastFM")}}</v-list-item-title>
<v-list-item-subtitle>Connect your LastFM account to allow scrobbling.</v-list-item-subtitle> <v-list-item-subtitle>{{$t("Connect your LastFM account to allow scrobbling.")}}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item v-if='$root.settings.lastFM' @click='disconnectLastFM'> <v-list-item v-if='$root.settings.lastFM' @click='disconnectLastFM'>
@ -97,32 +142,32 @@
<v-icon>mdi-logout</v-icon> <v-icon>mdi-logout</v-icon>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title class='red--text'>Disconnect LastFM</v-list-item-title> <v-list-item-title class='red--text'>{{$t("Disconnect LastFM")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Discord --> <!-- Discord -->
<v-list-item> <v-list-item>
<v-list-item-action> <v-list-item-action>
<v-checkbox v-model='$root.settings.enableDiscord' @click='snackbarText = "Requires restart to apply!"; snackbar = true'></v-checkbox> <v-checkbox v-model='$root.settings.enableDiscord' @click='snackbarText = $t("Requires restart to apply!"); snackbar = true'></v-checkbox>
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Discord Rich Presence</v-list-item-title> <v-list-item-title>{{$t("Discord Rich Presence")}}</v-list-item-title>
<v-list-item-subtitle>Enable Discord Rich Presence, requires restart to toggle!</v-list-item-subtitle> <v-list-item-subtitle>{{$t("Enable Discord Rich Presence, requires restart to toggle!")}}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Discord Join Button --> <!-- Discord Join Button -->
<v-list-item> <v-list-item>
<v-list-item-action> <v-list-item-action>
<v-checkbox v-model='$root.settings.discordJoin' @click='snackbarText = "Requires restart to apply!"; snackbar = true'></v-checkbox> <v-checkbox v-model='$root.settings.discordJoin' @click='snackbarText = $t("Requires restart to apply!"); snackbar = true'></v-checkbox>
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Discord Join Button</v-list-item-title> <v-list-item-title>{{$t("Discord Join Button")}}</v-list-item-title>
<v-list-item-subtitle>Enable Discord join button for syncing tracks, requires restart to toggle!</v-list-item-subtitle> <v-list-item-subtitle>{{$t("Enable Discord join button for syncing tracks, requires restart to toggle!")}}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Misc --> <!-- Misc -->
<v-subheader>Other</v-subheader> <v-subheader>{{$t("Other")}}</v-subheader>
<v-divider></v-divider> <v-divider></v-divider>
<!-- Minimize to tray --> <!-- Minimize to tray -->
@ -131,7 +176,7 @@
<v-checkbox v-model='$root.settings.minimizeToTray'></v-checkbox> <v-checkbox v-model='$root.settings.minimizeToTray'></v-checkbox>
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Minimize to tray</v-list-item-title> <v-list-item-title>{{$t("Minimize to tray")}}</v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Close on exit --> <!-- Close on exit -->
@ -140,22 +185,21 @@
<v-checkbox v-model='$root.settings.closeOnExit'></v-checkbox> <v-checkbox v-model='$root.settings.closeOnExit'></v-checkbox>
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Close on exit</v-list-item-title> <v-list-item-title>{{$t("Close on exit")}}</v-list-item-title>
<v-list-item-subtitle>Don't minimize to tray</v-list-item-subtitle> <v-list-item-subtitle>{{$t("Don't minimize to tray")}}</v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<!-- Logout --> <!-- Logout -->
<v-btn block color='red' class='mt-4' @click='logout'> <v-btn block color='red' class='mt-4' @click='logout'>
<v-icon>mdi-logout</v-icon> <v-icon>mdi-logout</v-icon>
Logout {{$t("Logout")}}
</v-btn> </v-btn>
</v-list> </v-list>
<v-btn class='my-4' large color='primary' :loading='saving' block @click='save'> <v-btn fab color='primary' absolute bottom right class='mb-12' @click='save' :loading='saving'>
<v-icon>mdi-content-save</v-icon> <v-icon>mdi-content-save</v-icon>
Save
</v-btn> </v-btn>
<!-- Info snackbar --> <!-- Info snackbar -->
@ -192,7 +236,17 @@ export default {
downloadQuality: null, downloadQuality: null,
devToolsCounter: 0, devToolsCounter: 0,
snackbarText: null, snackbarText: null,
snackbar: false snackbar: false,
language: 'en',
languages: [
{code: 'en', name: 'English'},
{code: 'ar', name: 'Arabic'},
{code: 'de', name: 'German'},
],
primaryColorIndex: 0,
primaries: ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4',
'#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800', '#FF5722',
'#795548', '#607D8B', '#9E9E9E', '#333333', '#000000']
} }
}, },
methods: { methods: {
@ -202,6 +256,8 @@ export default {
this.$root.saveSettings(); this.$root.saveSettings();
//Artificial wait to make it seem like something happened. //Artificial wait to make it seem like something happened.
setTimeout(() => {this.saving = false;}, 500); setTimeout(() => {this.saving = false;}, 500);
this.snackbarText = this.$t("Settings saved!");
this.snackbar = true;
}, },
getQuality(v) { getQuality(v) {
let i = this.qualities.indexOf(v); let i = this.qualities.indexOf(v);
@ -228,7 +284,7 @@ export default {
selectDownloadPath() { selectDownloadPath() {
//Electron check //Electron check
if (!this.$root.settings.electron) { if (!this.$root.settings.electron) {
alert('Available only in Electron version!'); alert(this.$t("Available only in Electron version!"));
return; return;
} }
const {ipcRenderer} = window.require('electron'); const {ipcRenderer} = window.require('electron');
@ -252,6 +308,24 @@ export default {
this.$root.settings.lastFM = null; this.$root.settings.lastFM = null;
await this.$root.saveSettings(); await this.$root.saveSettings();
window.location.reload(); window.location.reload();
},
changeColor() {
this.$vuetify.theme.themes.dark.primary = this.primaries[this.primaryColorIndex];
this.$root.settings.primaryColor = this.primaries[this.primaryColorIndex];
this.primaryColorIndex++;
if (this.primaryColorIndex == this.primaries.length)
this.primaryColorIndex = 0;
},
updateLanguage(l) {
let code = this.languages.filter(lang => lang.name == l)[0].code;
this.language = code;
this.$root.updateLanguage(code);
this.$root.settings.language = code;
}
},
computed: {
languageNames() {
return this.languages.map(l => l.name);
} }
}, },
mounted() { mounted() {
@ -272,6 +346,18 @@ export default {
remote.getCurrentWindow().toggleDevTools(); remote.getCurrentWindow().toggleDevTools();
} }
} }
//Shhhhhh
if (event.code == 'KeyC' && event.shiftKey) {
this.changeColor();
}
//SSHHSHSHHSH
if (event.code == 'KeyG' && event.shiftKey && event.altKey) {
setInterval(() => {
this.changeColor();
}, 400);
}
}); });
} }
} }

View File

@ -1,5 +1,14 @@
module.exports = { module.exports = {
"transpileDependencies": [ "transpileDependencies": [
"vuetify" "vuetify"
] ],
pluginOptions: {
i18n: {
locale: 'en',
fallbackLocale: 'en',
localeDir: 'locales',
enableInSFC: true
}
}
} }

6
app/package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "freezer", "name": "freezer",
"version": "1.0.9", "version": "1.0.10",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1505,8 +1505,8 @@
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
}, },
"nodeezcryptor": { "nodeezcryptor": {
"version": "git+https://notabug.org/xefglm/nodeezcryptor#26d049cba14fa1f5ee32a52f23f4eda05d9feeb4", "version": "git+https://codeberg.org/exttex/nodeezcryptor#78d99b64127256a1590d452b4804c4e38db24e97",
"from": "git+https://notabug.org/xefglm/nodeezcryptor", "from": "git+https://codeberg.org/exttex/nodeezcryptor",
"requires": { "requires": {
"bindings": "^1.5.0", "bindings": "^1.5.0",
"node-addon-api": "^2.0.0" "node-addon-api": "^2.0.0"

View File

@ -1,7 +1,7 @@
{ {
"name": "freezer", "name": "freezer",
"private": true, "private": true,
"version": "1.0.9", "version": "1.1.0",
"description": "", "description": "",
"main": "background.js", "main": "background.js",
"scripts": { "scripts": {
@ -17,7 +17,7 @@
"lastfmapi": "^0.1.1", "lastfmapi": "^0.1.1",
"metaflac-js2": "^1.0.7", "metaflac-js2": "^1.0.7",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"nodeezcryptor": "git+https://notabug.org/xefglm/nodeezcryptor", "nodeezcryptor": "git+https://codeberg.org/exttex/nodeezcryptor",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"socket.io": "^2.3.0", "socket.io": "^2.3.0",
"winston": "^3.3.3" "winston": "^3.3.3"

View File

@ -2,7 +2,8 @@ const crypto = require('crypto');
const axios = require('axios'); const axios = require('axios');
const decryptor = require('nodeezcryptor'); const decryptor = require('nodeezcryptor');
const querystring = require('querystring'); const querystring = require('querystring');
const {Transform} = require('stream'); const https = require('https');
const {Transform, Readable} = require('stream');
const {Track} = require('./definitions'); const {Track} = require('./definitions');
const logger = require('./winston'); const logger = require('./winston');
@ -63,6 +64,11 @@ class DeezerAPI {
} }
} }
//Invalid CSRF
if (data.data.error && data.data.error.VALID_TOKEN_REQUIRED) {
await this.callApi('deezer.getUserData');
return await this.callApi(method, args, gatewayInput);
}
return data.data; return data.data;
} }
@ -118,6 +124,13 @@ class DeezerAPI {
}); });
data = JSON.parse(data.toString('utf-8')); data = JSON.parse(data.toString('utf-8'));
//Invalid CSRF
if (data.error && data.error.VALID_TOKEN_REQUIRED) {
await this.callApi('deezer.getUserData');
return await this.callApi(method, args, gatewayInput);
}
return data; return data;
} }
@ -131,6 +144,15 @@ class DeezerAPI {
return true; return true;
} }
async callPublicApi(path, params) {
let res = await axios({
url: `https://api.deezer.com/${encodeURIComponent(path)}/${encodeURIComponent(params)}`,
responseType: 'json',
method: 'GET'
});
return res.data;
}
//Get track URL //Get track URL
static getUrl(trackId, md5origin, mediaVersion, quality = 3) { static getUrl(trackId, md5origin, mediaVersion, quality = 3) {
const magic = Buffer.from([0xa4]); const magic = Buffer.from([0xa4]);
@ -165,46 +187,118 @@ class DeezerAPI {
return `https://e-cdns-proxy-${md5origin.substring(0, 1)}.dzcdn.net/mobile/1/${step3}`; return `https://e-cdns-proxy-${md5origin.substring(0, 1)}.dzcdn.net/mobile/1/${step3}`;
} }
//Quality fallback
async qualityFallback(info, quality = 3) { async fallback(info, quality = 3) {
if (quality == 1) return { let qualityInfo = Track.getUrlInfo(info);
quality: '128kbps',
format: 'MP3', //User uploaded MP3s
source: 'stream', if (qualityInfo.trackId.startsWith('-')) {
url: `/stream/${info}?q=1` qualityInfo.quality = 3;
}; return qualityInfo;
}
//Quality fallback
let newQuality = await this.qualityFallback(qualityInfo, quality);
if (newQuality != null) {
return qualityInfo;
}
//ID Fallback
let trackData = await this.callApi('deezer.pageTrack', {sng_id: qualityInfo.trackId});
try { try {
let tdata = Track.getUrlInfo(info); if (trackData.results.DATA.FALLBACK.SNG_ID.toString() != qualityInfo.trackId) {
let res = await axios.head(DeezerAPI.getUrl(tdata.trackId, tdata.md5origin, tdata.mediaVersion, quality)); let newId = trackData.results.DATA.FALLBACK.SNG_ID.toString();
if (quality == 3) { let newTrackData = await this.callApi('deezer.pageTrack', {sng_id: newId});
return { let newTrack = new Track(newTrackData.results.DATA);
quality: '320kbps', return this.fallback(newTrack.streamUrl);
format: 'MP3',
source: 'stream',
url: `/stream/${info}?q=3`
}
}
//Bitrate will be calculated in client
return {
quality: res.headers['content-length'],
format: 'FLAC',
source: 'stream',
url: `/stream/${info}?q=9`
} }
} catch (e) { } catch (e) {
logger.warn('Qualiy fallback: ' + e); logger.warn('TrackID Fallback failed: ' + e);
}
//ISRC Fallback
try {
let publicTrack = this.callPublicApi('track', 'isrc:' + trackData.results.DATA.ISRC);
let newId = publicTrack.id.toString();
let newTrackData = await this.callApi('deezer.pageTrack', {sng_id: newId});
let newTrack = new Track(newTrackData.results.DATA);
return this.fallback(newTrack.streamUrl);
} catch (e) {
logger.warn('ISRC Fallback failed: ' + e);
}
return null;
}
//Fallback thru available qualities, -1 if none work
async qualityFallback(info, quality = 3) {
try {
let res = await axios.head(DeezerAPI.getUrl(info.trackId, info.md5origin, info.mediaVersion, quality));
if (res.status > 400) throw new Error(`Status code: ${res.status}`);
//Make sure it's an int
info.quality = parseInt(quality.toString(), 10);
info.size = parseInt(res.headers['content-length'], 10);
return info;
} catch (e) {
logger.warn('Quality fallback: ' + e);
//Fallback //Fallback
//9 - FLAC //9 - FLAC
//3 - MP3 320 //3 - MP3 320
//1 - MP3 128 //1 - MP3 128
let q = quality; let nq = -1;
if (quality == 9) q = 3; if (quality == 3) nq = 1;
if (quality == 3) q = 1; if (quality == 9) nq = 3;
return this.qualityFallback(info, q); if (quality == 1) return null;
return this.qualityFallback(info, nq);
} }
} }
} }
class DeezerStream extends Readable {
constructor(qualityInfo, options) {
super(options);
this.qualityInfo = qualityInfo;
this.ended = false;
}
async open(offset, end) {
//Prepare decryptor
this.decryptor = new DeezerDecryptionStream(this.qualityInfo.trackId, {offset});
this.decryptor.on('end', () => {
this.ended = true;
});
//Calculate headers
let offsetBytes = offset - (offset % 2048);
end = (end == -1) ? '' : end;
let url = DeezerAPI.getUrl(this.qualityInfo.trackId, this.qualityInfo.md5origin, this.qualityInfo.mediaVersion, this.qualityInfo.quality);
//Open request
await new Promise((res) => {
this.request = https.get(url, {headers: {'Range': `bytes=${offsetBytes}-${end}`}}, (r) => {
r.pipe(this.decryptor);
this.size = parseInt(r.headers['content-length'], 10) + offsetBytes;
res();
});
});
}
async _read() {
//Decryptor ended
if (this.ended)
return this.push(null);
this.decryptor.once('readable', () => {
this.push(this.decryptor.read());
});
}
_destroy(err, callback) {
this.request.destroy();
this.decryptor.destroy();
callback();
}
}
class DeezerDecryptionStream extends Transform { class DeezerDecryptionStream extends Transform {
constructor(trackId, options = {offset: 0}) { constructor(trackId, options = {offset: 0}) {
@ -258,4 +352,4 @@ class DeezerDecryptionStream extends Transform {
} }
module.exports = {DeezerAPI, DeezerDecryptionStream}; module.exports = {DeezerAPI, DeezerDecryptionStream, DeezerStream};

View File

@ -1,3 +1,4 @@
//Datatypes, constructor parameters = gw_light API call. //Datatypes, constructor parameters = gw_light API call.
class Track { class Track {
constructor(json) { constructor(json) {
@ -11,8 +12,8 @@ class Track {
this.artistString = this.artists.map((a) => a.name).join(', '); this.artistString = this.artists.map((a) => a.name).join(', ');
this.album = new Album(json); this.album = new Album(json);
this.trackNumber = parseInt((json.TRACK_NUMBER || 0).toString(), 10); this.trackNumber = parseInt((json.TRACK_NUMBER || 1).toString(), 10);
this.diskNumber = parseInt((json.DISK_NUMBER || 0).toString(), 10); this.diskNumber = parseInt((json.DISK_NUMBER || 1).toString(), 10);
this.explicit = json['EXPLICIT_LYRICS'] == 1 ? true:false; this.explicit = json['EXPLICIT_LYRICS'] == 1 ? true:false;
this.lyricsId = json.LYRICS_ID; this.lyricsId = json.LYRICS_ID;
@ -35,7 +36,7 @@ class Track {
if (info.charAt(32) == '1') md5origin += '.mp3'; if (info.charAt(32) == '1') md5origin += '.mp3';
let mediaVersion = parseInt(info.substring(33, 34)).toString(); let mediaVersion = parseInt(info.substring(33, 34)).toString();
let trackId = info.substring(35); let trackId = info.substring(35);
return {trackId, md5origin, mediaVersion}; return new QualityInfo(md5origin, mediaVersion, trackId);
} }
} }
@ -76,6 +77,7 @@ class Artist {
this.albumCount = albumsJson.total; this.albumCount = albumsJson.total;
this.albums = albumsJson.data.map((a) => new Album(a)); this.albums = albumsJson.data.map((a) => new Album(a));
this.topTracks = topJson.data.map((t) => new Track(t)); this.topTracks = topJson.data.map((t) => new Track(t));
this.radio = json.SMARTRADIO;
} }
} }
@ -277,5 +279,27 @@ class Lyric {
} }
} }
class QualityInfo {
constructor(md5origin, mediaVersion, trackId, quality = 1, source='stream') {
this.md5origin = md5origin;
this.mediaVersion = mediaVersion;
this.trackId = trackId;
this.quality = quality;
this.source = source;
//For FLAC bitrate calculation
this.size = 1;
this.url = '';
}
//Generate direct stream URL
generateUrl() {
let md5 = this.md5origin.replace('.mp3', '');
let md5mp3bit = this.md5origin.includes('.mp3') ? '1' : '0';
let mv = this.mediaVersion.toString().padStart(2, '0');
this.url = `/stream/${md5}${md5mp3bit}${mv}${this.trackId}?q=${this.quality}`;
}
}
module.exports = {Track, Album, Artist, Playlist, User, SearchResults, module.exports = {Track, Album, Artist, Playlist, User, SearchResults,
DeezerImage, DeezerProfile, DeezerLibrary, DeezerPage, Lyrics}; DeezerImage, DeezerProfile, DeezerLibrary, DeezerPage, Lyrics};

View File

@ -1,158 +1,46 @@
const {Settings} = require('./settings'); const {DeezerAPI} = require('./deezer');
const {Track} = require('./definitions');
const decryptor = require('nodeezcryptor');
const fs = require('fs');
const path = require('path');
const logger = require('./winston');
const https = require('https');
const Datastore = require('nedb'); const Datastore = require('nedb');
const {Settings} = require('./settings');
const fs = require('fs');
const https = require('https');
const logger = require('./winston');
const path = require('path');
const decryptor = require('nodeezcryptor');
const sanitize = require('sanitize-filename');
const ID3Writer = require('browser-id3-writer'); const ID3Writer = require('browser-id3-writer');
const Metaflac = require('metaflac-js2'); const Metaflac = require('metaflac-js2');
const sanitize = require("sanitize-filename"); const { Track, Lyrics } = require('./definitions');
const { DeezerAPI } = require('./deezer');
class Downloads { let deezer;
constructor(settings, qucb) {
this.downloads = [];
this.downloading = false;
this.download;
class DownloadManager {
constructor(settings, callback) {
this.settings = settings; this.settings = settings;
//Queue update callback
this.qucb = qucb;
}
//Add track to queue
async add(track, quality = null) {
if (this.downloads.filter((e => e.id == track.id)).length > 0) {
//Track already in queue
return;
}
//Sanitize quality
let q = this.settings.downloadsQuality;
if (quality) q = parseInt(quality.toString(), 10);
//Create download
let outpath = this.generateTrackPath(track, q);
let d = new Download(
track,
outpath,
q,
() => {this._downloadDone();}
);
this.downloads.push(d);
//Update callback
if (this.qucb) this.qucb();
//Save to DB
await new Promise((res, rej) => {
this.db.insert(d.toDB(), (e) => {
res();
});
});
}
generateTrackPath(track, quality) {
//Generate filename
let fn = this.settings.downloadFilename;
//Disable feats for single artist
let feats = '';
if (track.artists.length >= 2) feats = track.artists.slice(1).map((a) => a.name).join(', ');
let props = {
'%title%': track.title,
'%artists%': track.artistString,
'%artist%': track.artists[0].name,
'%feats%': feats,
'%trackNumber%': (track.trackNumber ? track.trackNumber : 1).toString(),
'%0trackNumber%': (track.trackNumber ? track.trackNumber : 1).toString().padStart(2, '0'),
'%album%': track.album.title
};
for (let k of Object.keys(props)) {
fn = fn.replace(new RegExp(k, 'g'), sanitize(props[k]));
}
//Generate folders
let p = this.settings.downloadsPath;
if (this.settings.createArtistFolder) p = path.join(p, sanitize(track.artists[0].name));
if (this.settings.createAlbumFolder) p = path.join(p, sanitize(track.album.title));
return path.join(p, fn);
}
async start() {
//Already downloading
if (this.download || this.downloads.length == 0) return;
this.downloading = true;
await this._downloadDone();
}
async stop() {
//Not downloading
if (!this.download || !this.downloading) return;
this.downloading = false; this.downloading = false;
await this.download.stop(); this.callback = callback;
//Back to queue if undone this.queue = [];
if (this.download.state < 3) this.downloads.unshift(this.download); this.threads = [];
this.download = null; this.updateRequests = 0;
//Update callback
if (this.qucb) this.qucb();
} }
//On download finished //Update DeezerAPI global
async _downloadDone() { setDeezer(d) {
//Save to DB deezer = d;
if (this.download) {
await new Promise((res, rej) => {
this.db.update({_id: this.download.id}, {
state: this.download.state,
fallback: this.download.fallback,
}, (e) => {
res();
});
// this.db.remove({_id: this.download.id}, (e) => {
// res();
// });
});
}
this.download = null;
//All downloads done
if (this.downloads.length == 0 || this.downloading == false) {
this.downloading = false;
if (this.qucb) this.qucb();
return;
}
this.download = this.downloads[0];
this.downloads = this.downloads.slice(1);
this.download.start();
//Update callback
if (this.qucb) this.qucb();
} }
//Load downloads info
async load() { async load() {
this.db = new Datastore({filename: Settings.getDownloadsDB(), autoload: true}); this.db = new Datastore({filename: Settings.getDownloadsDB(), autoload: true});
//Load downloads
await new Promise((res, rej) => {
this.db.find({}, (err, docs) => {
if (err) return rej();
if (!docs) return;
for (let d of docs) { //Load from DB
if (d.state < 3 && d.state >= 0) this.downloads.push(Download.fromDB(d, () => {this._downloadDone();})); await new Promise((resolve) => {
//TODO: Ignore for now completed this.db.find({state: 0}, (err, docs) => {
if (!err) {
this.queue = docs.map(d => Download.fromDB(d));
} }
res(); resolve();
}); });
}); });
@ -162,138 +50,211 @@ class Downloads {
} }
} }
//Remove download async start() {
async delete(index) { this.downloading = true;
//Clear all await this.updateQueue();
if (index == -1) { }
this.downloads = [];
await new Promise((res, rej) => {
this.db.remove({state: 0}, {multi: true}, (e) => {});
res();
});
if (this.qucb) this.qucb(); async stop() {
this.downloading = false;
//Stop all threads
let nThreads = this.threads.length;
for (let i=nThreads-1; i>=0; i--) {
await this.threads[i].stop();
}
this.updateQueue();
}
async add(track, quality) {
//Sanitize quality
let q = this.settings.downloadsQuality;
if (quality)
q = parseInt(quality.toString(), 10);
let download = new Download(track, q, 0);
//Check if in queue
if (this.queue.some(d => d.track.id == track.id)) {
return; return;
} }
//Remove single //Check if in DB
if (index >= this.downloads.length) return; let dbDownload = await new Promise((resolve) => {
await new Promise((res, rej) => { this.db.find({_id: download.track.id}, (err, docs) => {
this.db.remove({_id: this.downloads[index].id}, {}, (e) => {}); if (err) return resolve(null);
res(); if (docs.length == 0) return resolve(null);
});
this.downloads.splice(index, 1);
if (this.qucb) this.qucb(); //Update download as not done, will be skipped while downloading
this.db.update({_id: download.track.id}, {state: 0, quality: download.quality}, {}, () => {
resolve(Download.fromDB(docs[0]));
});
});
});
//Insert to DB
if (!dbDownload) {
await new Promise((resolve) => {
this.db.insert(download.toDB(), () => {
resolve();
});
});
}
//Queue
this.queue.push(download);
this.updateQueue();
}
async delete(index) {
//-1 = Delete all
if (index == -1) {
let ids = this.queue.map(q => q.track.id);
this.queue = [];
//Remove from DB
await new Promise((res) => {
this.db.remove({_id: {$in: ids}}, {multi: true}, () => {
res();
})
});
this.updateQueue();
return;
}
//Remove single item
let id = this.queue[index].track.id;
this.queue.splice(index, 1);
await new Promise((res) => {
this.db.remove({_id: id}, {}, () => {
res();
})
})
this.updateQueue();
}
//Thread safe update
async updateQueue() {
this.updateRequests++;
if (this._updatePromise) return;
this._updatePromise = this._updateQueue();
await this._updatePromise;
this._updatePromise = null;
this.updateRequests--;
if (this.updateRequests > 0) {
this.updateRequests--;
this.updateQueue();
}
}
async _updateQueue() {
//Finished downloads
if (this.threads.length > 0) {
for (let i=this.threads.length-1; i>=0; i--) {
if (this.threads[i].download.state == 3 || this.threads[i].download.state == -1) {
//Update DB
await new Promise((resolve) => {
this.db.update({_id: this.threads[i].download.track.id}, {state: this.threads[i].download.state}, {}, () => {
resolve();
});
});
this.threads.splice(i, 1);
} else {
//Remove if stopped
if (this.threads[i].stopped) {
this.queue.unshift(this.threads[i].download);
this.threads.splice(i, 1);
}
}
}
}
//Create new threads
if (this.downloading) {
let nThreads = this.settings.downloadThreads - this.threads.length;
for (let i=0; i<nThreads; i++) {
if (this.queue.length > 0) {
let thread = new DownloadThread(this.queue[0], () => {this.updateQueue();}, this.settings);
thread.start();
this.threads.push(thread);
this.queue.splice(0, 1);
}
}
}
//Stop downloading if queues empty
if (this.queue.length == 0 && this.threads.length == 0 && this.downloading)
this.downloading = false;
//Update UI
if (this.callback)
this.callback();
} }
} }
class Download { class DownloadThread {
constructor(track, path, quality, onDone) { constructor (download, callback, settings) {
this.track = track; this.download = download;
this.id = track.id; this.callback = callback;
this.path = path; this.settings = settings;
this.quality = quality; this.stopped = true;
this.onDone = onDone; this.isUserUploaded = download.track.id.toString().startsWith('-');
//States:
//0 - none/stopped
//1 - downloading
//2 - post-processing
//3 - done
//-1 - download error
this.state = 0;
this.fallback = false;
this._request;
//Post Processing Promise
this._ppp;
this.downloaded = 0;
this.size = 0;
} }
//Serialize to database json //Callback wrapper
toDB() { _cb() {
return { if (this.callback) this.callback();
_id: this.id,
path: this.path,
quality: this.quality,
track: this.track,
state: this.state,
fallback: this.fallback
}
}
//Create download from DB document
static fromDB(doc, onDone) {
let d = new Download(doc.track, doc.path, doc.quality, onDone);
d.fallback = doc.fallback ? true : false; //Null check
d.state = doc.state;
return d;
} }
async start() { async start() {
this.state = 1; this.download.state = 1;
this.stopped = false;
//Fallback
this.qualityInfo = await deezer.fallback(this.download.track.streamUrl, this.download.quality);
if (!this.qualityInfo) {
this.download.state = -1;
return;
}
//Get track info
if (!this.isUserUploaded) {
this.rawTrack = await deezer.callApi('deezer.pageTrack', {'sng_id': this.download.track.id});
this.track = new Track(this.rawTrack.results.DATA);
this.publicTrack = await deezer.callPublicApi('track', this.track.id);
this.publicAlbum = await deezer.callPublicApi('album', this.track.album.id);
}
//Check if exists
let outPath = this.generatePath(this.qualityInfo.quality);
try {
await fs.promises.access(outPath, fs.constants.R_OK);
//File exists
this.download.state = 3;
return this._cb();
} catch (_) {}
//Path to temp file //Path to temp file
let tmp = path.join(Settings.getTempDownloads(), `${this.track.id}.ENC`); let tmp = path.join(Settings.getTempDownloads(), `${this.download.track.id}.ENC`);
//Get start offset //Get start offset
let start = 0; let start = 0;
try { try {
let stat = await fs.promises.stat(tmp); let stat = await fs.promises.stat(tmp);
if (stat.size) start = stat.size; if (stat.size) start = stat.size;
// eslint-disable-next-line no-empty
} catch (e) {} } catch (e) {}
this.downloaded = start; this.download.downloaded = start;
//Get download info //Download
let streamInfo = Track.getUrlInfo(this.track.streamUrl); let url = DeezerAPI.getUrl(this.qualityInfo.trackId, this.qualityInfo.md5origin, this.qualityInfo.mediaVersion, this.qualityInfo.quality);
this.url = DeezerAPI.getUrl(streamInfo.trackId, streamInfo.md5origin, streamInfo.mediaVersion, this.quality); if (this.stopped) return;
this._request = https.get(this.url, {headers: {'Range': `bytes=${start}-`}}, (r) => { this._request = https.get(url, {headers: {'Range': `bytes=${start}-`}}, (r) => {
this._response = r;
let outFile = fs.createWriteStream(tmp, {flags: 'a'}); let outFile = fs.createWriteStream(tmp, {flags: 'a'});
let skip = false;
//Error
if (r.statusCode >= 400) {
//Fallback on error
if (this.quality > 1) {
if (this.quality == 3) this.quality = 1;
if (this.quality == 9) this.quality = 3;
this.url = null;
this.fallback = true;
return this.start();
};
//Error
this.state = -1;
logger.error(`Undownloadable track ID: ${this.track.id}`);
return this.onDone();
} else {
this.path += (this.quality == 9) ? '.flac' : '.mp3';
//Check if file exits
fs.access(this.path, (err) => {
if (err) {
} else {
logger.warn('File already exists! Skipping...');
outFile.close();
skip = true;
this._request.end();
this.state = 3;
return this.onDone();
}
})
}
//On download done //On download done
r.on('end', () => { r.on('end', () => {
if (skip) return; if (this.download.size != this.download.downloaded) return;
if (this.downloaded != this.size) return;
outFile.on('finish', () => { outFile.on('finish', () => {
outFile.close(() => { outFile.close(() => {
this._finished(tmp); this.postPromise = this._post(tmp);
}); });
}); });
outFile.end(); outFile.end();
@ -301,7 +262,7 @@ class Download {
//Progress //Progress
r.on('data', (c) => { r.on('data', (c) => {
outFile.write(c); outFile.write(c);
this.downloaded += c.length; this.download.downloaded += c.length;
}); });
r.on('error', (e) => { r.on('error', (e) => {
@ -311,53 +272,136 @@ class Download {
//Save size //Save size
this.size = parseInt(r.headers['content-length'], 10) + start; this.size = parseInt(r.headers['content-length'], 10) + start;
this.download.size = this.size;
}); });
} }
//Stop current request
async stop() { async stop() {
this._request.destroy(); //If post processing, wait for it
this._request = null; if (this.postPromise) {
this.state = 0; await this._postPromise;
if (this._ppp) await this._ppp; return this._cb();
}
//Cancel download
if (this._response)
this._response.destroy();
if (this._request)
this._request.destroy();
// this._response = null;
// this._request = null;
this.stopped = true;
this.download.state = 0;
this._cb();
} }
async _finished(tmp) { async _post(tmp) {
this.state = 2; this.download.state = 2;
//Create post processing promise
let resolve;
this._ppp = new Promise((res, rej) => {
resolve = res;
});
//Prepare output directory
try {
await fs.promises.mkdir(path.dirname(this.path), {recursive: true})
} catch (e) {};
//Decrypt //Decrypt
//this.path += (this.quality == 9) ? '.flac' : '.mp3'; decryptor.decryptFile(decryptor.getKey(this.qualityInfo.trackId), tmp, `${tmp}.DEC`);
decryptor.decryptFile(decryptor.getKey(this.track.id), tmp, `${tmp}.DEC`); let outPath = this.generatePath(this.qualityInfo.quality);
await fs.promises.copyFile(`${tmp}.DEC`, this.path); await fs.promises.mkdir(path.dirname(outPath), {recursive: true});
//Delete encrypted await fs.promises.copyFile(`${tmp}.DEC`, outPath);
await fs.promises.unlink(tmp);
await fs.promises.unlink(`${tmp}.DEC`); await fs.promises.unlink(`${tmp}.DEC`);
await fs.promises.unlink(tmp);
//Tags if (!this.isUserUploaded) {
await this.tagAudio(this.path, this.track); //Tag
await this.tagTrack(outPath);
//Finish //Lyrics
this.state = 3; if (this.settings.downloadLyrics) {
resolve(); let lrcFile = outPath.split('.').slice(0, -1).join('.') + '.lrc';
this._ppp = null; let lrc;
this.onDone(); try {
lrc = this.generateLRC();
} catch (e) {
logger.warn('Error getting lyrics! ' + e);
}
if (lrc) {
await fs.promises.writeFile(lrcFile, lrc, {encoding: 'utf-8'});
}
}
}
this.download.state = 3;
this._cb();
}
async tagTrack(path) {
let cover;
try {
cover = await this.downloadCover(this.track.albumArt.full);
} catch (e) {}
//Genre tag
let genres = [];
if (this.publicAlbum.genres && this.publicAlbum.genres.data)
genres = this.publicAlbum.genres.data.map(g => g.name);
if (path.toLowerCase().endsWith('.mp3')) {
//Load
const audioData = await fs.promises.readFile(path);
const writer = new ID3Writer(audioData);
writer.setFrame('TIT2', this.track.title);
writer.setFrame('TPE1', this.track.artists.map((a) => a.name));
if (this.publicAlbum.artist) writer.setFrame('TPE2', this.publicAlbum.artist.name);
writer.setFrame('TALB', this.track.album.title);
writer.setFrame('TRCK', this.track.trackNumber);
writer.setFrame('TPOS', this.track.diskNumber);
writer.setFrame('TCON', genres);
let date = new Date(this.publicTrack.release_date);
writer.setFrame('TYER', date.getFullYear());
writer.setFrame('TDAT', `${date.getMonth().toString().padStart(2, '0')}${date.getDay().toString().padStart(2, '0')}`);
if (this.publicTrack.bpm > 2) writer.setFrame('TBPM', this.publicTrack.bpm);
if (this.publicAlbum.label) writer.setFrame('TPUB', this.publicAlbum.label);
writer.setFrame('TSRC', this.publicTrack.isrc);
if (this.rawTrack.results.LYRICS) writer.setFrame('USLT', {
lyrics: this.rawTrack.results.LYRICS.LYRICS_TEXT,
language: 'eng',
description: 'Unsychronised lyrics'
});
if (cover) writer.setFrame('APIC', {type: 3, data: cover, description: 'Cover'});
writer.addTag();
//Write
await fs.promises.writeFile(path, Buffer.from(writer.arrayBuffer));
return;
}
//Tag FLAC
if (path.toLowerCase().endsWith('.flac')) {
const flac = new Metaflac(path);
flac.removeAllTags();
flac.setTag(`TITLE=${this.track.title}`);
flac.setTag(`ALBUM=${this.track.album.title}`);
flac.setTag(`ARTIST=${this.track.artistString}`);
flac.setTag(`TRACKNUMBER=${this.track.trackNumber}`);
flac.setTag(`DISCNUMBER=${this.track.diskNumber}`);
if (this.publicAlbum.artist) flac.setTag(`ALBUMARTIST=${this.publicAlbum.artist.name}`);
flac.setTag(`GENRE=${genres.join(", ")}`);
flac.setTag(`DATE=${this.publicTrack.release_date}`);
if (this.publicTrack.bpm > 2) flac.setTag(`BPM=${this.publicTrack.bpm}`);
if (this.publicAlbum.label) flac.setTag(`LABEL=${this.publicAlbum.label}`);
flac.setTag(`ISRC=${this.publicTrack.isrc}`);
if (this.publicAlbum.upc) flac.setTag(`BARCODE=${this.publicAlbum.upc}`);
if (this.rawTrack.results.LYRICS) flac.setTag(`LYRICS=${this.rawTrack.results.LYRICS.LYRICS_TEXT}`);
if (cover) flac.importPicture(cover);
flac.save();
}
} }
//Download cover to buffer
async downloadCover(url) { async downloadCover(url) {
return await new Promise((res, rej) => { return await new Promise((res) => {
let out = Buffer.alloc(0); let out = Buffer.alloc(0);
https.get(url, (r) => { https.get(url, (r) => {
r.on('data', (d) => { r.on('data', (d) => {
@ -370,49 +414,105 @@ class Download {
}); });
} }
//Write tags to audio file generateLRC() {
async tagAudio(path, track) { //Check if exists
let cover; if (!this.rawTrack.results.LYRICS || !this.rawTrack.results.LYRICS.LYRICS_SYNC_JSON) return;
try { let lyrics = new Lyrics(this.rawTrack.results.LYRICS);
cover = await this.downloadCover(track.albumArt.full); if (lyrics.lyrics.length == 0) return;
} catch (e) {} //Metadata
let out = `[ar:${this.track.artistString}]\r\n[al:${this.track.album.title}]\r\n[ti:${this.track.title}]\r\n`;
//Lyrics
if (path.toLowerCase().endsWith('.mp3')) { for (let l of lyrics.lyrics) {
//Load if (l.lrcTimestamp && l.text)
const audioData = await fs.promises.readFile(path); out += `${l.lrcTimestamp}${l.text}\r\n`;
const writer = new ID3Writer(audioData);
writer.setFrame('TIT2', track.title);
if (track.artists) writer.setFrame('TPE1', track.artists.map((a) => a.name));
if (track.album) writer.setFrame('TALB', track.album.title);
if (track.trackNumber) writer.setFrame('TRCK', track.trackNumber);
if (cover) writer.setFrame('APIC', {
type: 3,
data: cover,
description: 'Cover'
});
writer.addTag();
//Write
await fs.promises.writeFile(path, Buffer.from(writer.arrayBuffer));
} }
//Tag FLAC return out;
if (path.toLowerCase().endsWith('.flac')) { }
const flac = new Metaflac(path);
flac.removeAllTags();
flac.setTag(`TITLE=${track.title}`); generatePath(quality) {
if (track.album)flac.setTag(`ALBUM=${track.album.title}`); //User uploaded mp3s
if (track.trackNumber) flac.setTag(`TRACKNUMBER=${track.trackNumber}`); if (this.isUserUploaded) {
if (track.artistString) flac.setTag(`ARTIST=${track.artistString}`); //Generate path
if (cover) flac.importPicture(cover); let p = this.settings.downloadsPath;
if (this.settings.createArtistFolder && this.download.track.artists[0].name.length > 0)
flac.save(); p = path.join(p, sanitize(this.download.track.artists[0].name));
if (this.settings.createAlbumFolder && this.download.track.album.title.length > 0)
p = path.join(p, sanitize(this.download.track.album.title));
//Filename
let out = path.join(p, sanitize(this.download.track.title));
if (!out.includes('.'))
out += '.mp3';
return out;
} }
//Generate filename
let fn = this.settings.downloadFilename;
//Disable feats for single artist
let feats = '';
if (this.track.artists.length >= 2)
feats = this.track.artists.slice(1).map((a) => a.name).join(', ');
//Date
let date = new Date(this.publicTrack.release_date);
let props = {
'%title%': this.track.title,
'%artists%': this.track.artistString,
'%artist%': this.track.artists[0].name,
'%feats%': feats,
'%trackNumber%': (this.track.trackNumber ? this.track.trackNumber : 1).toString(),
'%0trackNumber%': (this.track.trackNumber ? this.track.trackNumber : 1).toString().padStart(2, '0'),
'%album%': this.track.album.title,
'%year%': date.getFullYear().toString(),
};
for (let k of Object.keys(props)) {
fn = fn.replace(new RegExp(k, 'g'), sanitize(props[k]));
}
//Generate folders
let p = this.settings.downloadsPath;
if (this.settings.createArtistFolder) p = path.join(p, sanitize(this.track.artists[0].name));
if (this.settings.createAlbumFolder) p = path.join(p, sanitize(this.track.album.title));
//Extension
if (quality.toString() == '9') {
fn += '.flac';
} else {
fn += '.mp3';
}
return path.join(p, fn);
} }
} }
class Download {
constructor (track, quality, state) {
this.track = track;
this.quality = quality;
// 0 - none
// 1 - downloading
// 2 - postprocess
// 3 - done
// -1 - error
this.state = state;
module.exports = {Downloads, Download}; //Updated from threads
this.downloaded = 0;
this.size = 1;
}
toDB() {
return {
_id: this.track.id,
track: this.track,
quality: this.quality,
state: this.state
}
}
static fromDB(json) {
return new Download(json.track, json.quality, json.state);
}
}
module.exports = {DownloadManager}

View File

@ -4,15 +4,15 @@ const https = require('https');
const fs = require('fs'); const fs = require('fs');
const axios = require('axios').default; const axios = require('axios').default;
const logger = require('./winston'); const logger = require('./winston');
const {DeezerAPI, DeezerDecryptionStream} = require('./deezer'); const {DeezerAPI, DeezerStream} = require('./deezer');
const {Settings} = require('./settings'); const {Settings} = require('./settings');
const {Track, Album, Artist, Playlist, DeezerProfile, SearchResults, DeezerLibrary, DeezerPage, Lyrics} = require('./definitions'); const {Track, Album, Artist, Playlist, DeezerProfile, SearchResults, DeezerLibrary, DeezerPage, Lyrics} = require('./definitions');
const {Downloads} = require('./downloads'); const {DownloadManager} = require('./downloads');
const {Integrations} = require('./integrations'); const {Integrations} = require('./integrations');
let settings; let settings;
let deezer; let deezer;
let downloads; let downloadManager;
let integrations; let integrations;
let sockets = []; let sockets = [];
@ -23,13 +23,16 @@ app.use(express.json({limit: '50mb'}));
app.use(express.static(path.join(__dirname, '../client', 'dist'))); app.use(express.static(path.join(__dirname, '../client', 'dist')));
//Server //Server
const server = require('http').createServer(app); const server = require('http').createServer(app);
const io = require('socket.io').listen(server); const io = require('socket.io').listen(server, {
path: '/socket',
});
//Get playback info //Get playback info
app.get('/playback', async (req, res) => { app.get('/playback', async (req, res) => {
try { try {
let data = await fs.promises.readFile(Settings.getPlaybackInfoPath(), 'utf-8'); let data = await fs.promises.readFile(Settings.getPlaybackInfoPath(), 'utf-8');
return res.json(data); return res.json(data);
// eslint-disable-next-line no-empty
} catch (e) {} } catch (e) {}
return res.json({}); return res.json({});
@ -53,7 +56,7 @@ app.get('/settings', (req, res) => {
app.post('/settings', async (req, res) => { app.post('/settings', async (req, res) => {
if (req.body) { if (req.body) {
Object.assign(settings, req.body); Object.assign(settings, req.body);
downloads.settings = settings; downloadManager.settings = settings;
integrations.updateSettings(settings); integrations.updateSettings(settings);
await settings.save(); await settings.save();
} }
@ -70,6 +73,9 @@ app.post('/authorize', async (req, res) => {
settings.arl = req.body.arl; settings.arl = req.body.arl;
if (await (deezer.authorize())) { if (await (deezer.authorize())) {
//Update download manager
downloadManager.setDeezer(deezer);
res.status(200).send('OK'); res.status(200).send('OK');
return; return;
} }
@ -238,16 +244,22 @@ app.put('/library/:type', async (req, res) => {
app.get('/streaminfo/:info', async (req, res) => { app.get('/streaminfo/:info', async (req, res) => {
let info = req.params.info; let info = req.params.info;
let quality = req.query.q ? req.query.q : 3; let quality = req.query.q ? req.query.q : 3;
return res.json(await deezer.qualityFallback(info, quality)); let qualityInfo = await deezer.fallback(info, quality);
if (qualityInfo == null)
return res.sendStatus(404).end();
//Generate stream URL before sending
qualityInfo.generateUrl();
return res.json(qualityInfo);
}); });
// S T R E A M I N G // S T R E A M I N G
app.get('/stream/:info', (req, res) => { app.get('/stream/:info', async (req, res) => {
//Parse stream info //Parse stream info
let quality = req.query.q ? req.query.q : 3; let quality = req.query.q ? req.query.q : 3;
let streamInfo = Track.getUrlInfo(req.params.info); let streamInfo = Track.getUrlInfo(req.params.info);
let url = DeezerAPI.getUrl(streamInfo.trackId, streamInfo.md5origin, streamInfo.mediaVersion, quality); streamInfo.quality = quality;
let trackId = req.params.info.substring(35);
//MIME type of audio //MIME type of audio
let mime = 'audio/mp3'; let mime = 'audio/mp3';
@ -258,59 +270,38 @@ app.get('/stream/:info', (req, res) => {
if (req.headers.range) range = req.headers.range; if (req.headers.range) range = req.headers.range;
let rangeParts = range.replace(/bytes=/, '').split('-'); let rangeParts = range.replace(/bytes=/, '').split('-');
let start = parseInt(rangeParts[0], 10); let start = parseInt(rangeParts[0], 10);
let end = ''; let end = -1;
if (rangeParts.length >= 2) end = rangeParts[1]; if (rangeParts.length >= 2) end = rangeParts[1];
if (end == '' || end == ' ') end = -1;
//Round to 2048 for deezer //Create Stream
let dStart = start - (start % 2048); let stream = new DeezerStream(streamInfo, {});
await stream.open(start, end);
//Make request to Deezer CDN //Range header
let _request = https.get(url, {headers: {'Range': `bytes=${dStart}-${end}`}}, (r) => { if (req.headers.range) {
//Error from Deezer end = (end == -1) ? stream.size - 1 : end;
//TODO: Quality fallback res.writeHead(206, {
if (r.statusCode < 200 || r.statusCode > 300) { 'Content-Range': `bytes ${start}-${end}/${stream.size}`,
res.status(404); 'Accept-Ranges': 'bytes',
return res.end(); 'Content-Length': stream.size - start,
} 'Content-Type': mime
});
let decryptor = new DeezerDecryptionStream(trackId, {offset: start}); //Normal (non range) request
} else {
res.writeHead(200, {
'Content-Length': stream.size,
'Content-Type': mime
});
}
//Get total size //Should force HTML5 to retry
let chunkSize = parseInt(r.headers["content-length"], 10) stream.on('error', () => {
let total = chunkSize;
if (start > 0) total += start;
//Ranged request
if (req.headers.range) {
end = total - 1
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${total}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize,
'Content-Type': mime
});
//Normal (non range) request
} else {
res.writeHead(200, {
'Content-Length': total,
'Content-Type': mime
});
}
//Pipe: Deezer -> Decryptor -> Response
decryptor.pipe(res);
r.pipe(decryptor);
});
//Internet/Request error
_request.on('error', () => {
//console.log('Streaming error: ' + e);
//HTML audio will restart automatically
res.destroy(); res.destroy();
}); });
stream.pipe(res);
}); });
//Get deezer page //Get deezer page
@ -361,6 +352,18 @@ app.get('/smarttracklist/:id', async (req, res) => {
return res.send(tracks); return res.send(tracks);
}); });
//Artist smart radio
app.get('/smartradio/:id', async (req, res) => {
let data = await deezer.callApi('smart.getSmartRadio', {art_id: req.params.id});
res.send(data.results.data.map(t => new Track(t)));
});
//Track Mix
app.get('/trackmix/:id', async (req, res) => {
let data = await deezer.callApi('song.getContextualTrackMix', {sng_ids: [req.params.id]});
res.send(data.results.data.map(t => new Track(t)));
});
//Load lyrics, ID = SONG ID //Load lyrics, ID = SONG ID
app.get('/lyrics/:id', async (req, res) => { app.get('/lyrics/:id', async (req, res) => {
let data = await deezer.callApi('song.getLyrics', { let data = await deezer.callApi('song.getLyrics', {
@ -390,7 +393,7 @@ app.post('/downloads', async (req, res) => {
let tracks = req.body; let tracks = req.body;
let quality = req.query.q; let quality = req.query.q;
for (let track of tracks) { for (let track of tracks) {
downloads.add(track, quality); downloadManager.add(track, quality);
} }
res.status(200).send('OK'); res.status(200).send('OK');
@ -398,30 +401,29 @@ app.post('/downloads', async (req, res) => {
//PUT to /download to start //PUT to /download to start
app.put('/download', async (req, res) => { app.put('/download', async (req, res) => {
await downloads.start(); await downloadManager.start();
res.status(200).send('OK'); res.status(200).send('OK');
}); });
//DELETE to /download to stop/pause //DELETE to /download to stop/pause
app.delete('/download', async (req, res) => { app.delete('/download', async (req, res) => {
await downloads.stop(); await downloadManager.stop();
res.status(200).send('OK'); res.status(200).send('OK');
}) })
//Get all downloads //Get all downloads
app.get('/downloads', async (req, res) => { app.get('/downloads', async (req, res) => {
res.json({ res.json({
downloading: downloads.downloading, downloading: downloadManager.downloading,
downloads: downloads.downloads.map((d) => { queue: downloadManager.queue,
return d.toDB(); threads: downloadManager.threads.map(t => t.download)
})
}); });
}); });
//Delete singel download //Delete single download
app.delete('/downloads/:index', async (req, res) => { app.delete('/downloads/:index', async (req, res) => {
let index = parseInt(req.params.index, 10); let index = parseInt(req.params.index, 10);
await downloads.delete(index); await downloadManager.delete(index);
res.status(200).end(); res.status(200).end();
}); });
@ -499,32 +501,27 @@ async function createServer(electron = false, ecb) {
deezer = new DeezerAPI(settings.arl, electron); deezer = new DeezerAPI(settings.arl, electron);
//Prepare downloads //Prepare downloads
downloads = new Downloads(settings, () => { downloadManager = new DownloadManager(settings, () => {
//Emit queue change to socket //Emit queue change to socket
sockets.forEach((s) => { sockets.forEach((s) => {
s.emit('downloads', { s.emit('downloads', {
downloading: downloads.downloading, downloading: downloadManager.downloading,
downloads: downloads.downloads queue: downloadManager.queue,
threads: downloadManager.threads.map(t => t.download)
}); });
}); });
}); });
await downloads.load(); await downloadManager.load();
downloadManager.setDeezer(deezer);
//Emit download progress updates //Emit download progress updates
setInterval(() => { setInterval(() => {
sockets.forEach((s) => { sockets.forEach((s) => {
if (!downloads.download) { if (!downloadManager.downloading && downloadManager.threads.length == 0)
s.emit('download', null);
return; return;
}
s.emit('download', { s.emit('currentlyDownloading', downloadManager.threads.map(t => t.download));
id: downloads.download.id,
size: downloads.download.size,
downloaded: downloads.download.downloaded,
track: downloads.download.track,
path: downloads.download.path
});
}); });
}, 350); }, 400);
//Integrations (lastfm, discord) //Integrations (lastfm, discord)
integrations = new Integrations(settings); integrations = new Integrations(settings);

View File

@ -28,6 +28,12 @@ class Settings {
this.lastFM = null; this.lastFM = null;
this.enableDiscord = false; this.enableDiscord = false;
this.discordJoin = false; this.discordJoin = false;
this.showAutocomplete = true;
this.downloadThreads = 4;
this.downloadLyrics = true;
this.primaryColor = '#2196F3';
this.language = 'en';
} }
//Based on electorn app.getPath //Based on electorn app.getPath
@ -57,7 +63,12 @@ class Settings {
} }
//Get path to downloads database //Get path to downloads database
static getDownloadsDB() { static getDownloadsDB() {
return path.join(Settings.getDir(), 'downloads.db'); //Delete old DB if exists
let oldPath = path.join(Settings.getDir(), 'downloads.db');
if (fs.existsSync(oldPath))
fs.unlink(oldPath, () => {});
return path.join(Settings.getDir(), 'downloads2.db');
} }
//Get path to temporary / unfinished downlaods //Get path to temporary / unfinished downlaods
static getTempDownloads() { static getTempDownloads() {

BIN
build/installerIcon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
build/uninstallerIcon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

17
generate_translations.py Normal file
View File

@ -0,0 +1,17 @@
import zipfile
import json
def generate():
with zipfile.ZipFile('translations.zip') as zip:
for file in zip.namelist():
if 'freezerpc.json' in file:
data = zip.open(file).read()
lang = file.split('/')[0].split('-')[0].lower()
if lang != 'en':
with open('app/client/src/locales/' + lang + '.json', 'wb') as f:
f.write(data)
if __name__ == '__main__':
generate()

View File

@ -1,7 +1,7 @@
{ {
"name": "freezer", "name": "freezer",
"private": true, "private": true,
"version": "1.0.9", "version": "1.1.0",
"description": "", "description": "",
"scripts": { "scripts": {
"pack": "electron-builder --dir", "pack": "electron-builder --dir",
@ -16,6 +16,7 @@
}, },
"build": { "build": {
"appId": "com.exttex.freezer", "appId": "com.exttex.freezer",
"productName": "Freezer",
"extraResources": [ "extraResources": [
{ {
"from": "app/assets/**", "from": "app/assets/**",
@ -29,13 +30,19 @@
], ],
"win": { "win": {
"target": [ "target": [
"portable" "portable", "nsis"
], ],
"icon": "build/icon.ico", "icon": "build/icon.ico",
"asarUnpack": [ "asarUnpack": [
"app/node_modules/nodeezcryptor/**" "app/node_modules/nodeezcryptor/**"
] ]
}, },
"nsis": {
"oneClick": true,
"perMachine": false,
"allowElevation": false,
"allowToChangeInstallationDirectory": false
},
"linux": { "linux": {
"target": [ "target": [
"AppImage" "AppImage"