Merge branch 'advplyr:master' into master

This commit is contained in:
Hallo951 2022-11-11 08:30:54 +01:00 committed by GitHub
commit d2aabde8fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 559 additions and 55 deletions

View File

@ -4,7 +4,7 @@
<app-side-rail v-if="isShowingSideRail" class="hidden md:block" />
<div id="app-content" class="h-full" :class="{ 'has-siderail': isShowingSideRail }">
<Nuxt />
<Nuxt :key="currentLang" />
</div>
<app-stream-container ref="streamContainer" />
@ -31,7 +31,8 @@ export default {
socket: null,
isSocketConnected: false,
isFirstSocketConnection: true,
socketConnectionToastId: null
socketConnectionToastId: null,
currentLang: null
}
},
watch: {
@ -519,6 +520,10 @@ export default {
.catch((error) => {
console.error('Failed to load tasks', error)
})
},
changeLanguage(code) {
console.log('Changed lang', code)
this.currentLang = code
}
},
beforeMount() {
@ -527,6 +532,7 @@ export default {
mounted() {
this.updateBodyClass()
this.resize()
this.$eventBus.$on('change-lang', this.changeLanguage)
window.addEventListener('resize', this.resize)
window.addEventListener('keydown', this.keyDown)
@ -544,6 +550,7 @@ export default {
}
},
beforeDestroy() {
this.$eventBus.$off('change-lang', this.changeLanguage)
window.removeEventListener('resize', this.resize)
window.removeEventListener('keydown', this.keyDown)
}

View File

@ -12,8 +12,12 @@
<ui-text-input-with-label disabled :value="usertype" :label="$strings.LabelAccountType" />
</div>
</div>
<div class="py-4">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelLanguage }}</p>
<ui-dropdown v-model="selectedLanguage" :items="$languageCodeOptions" small class="max-w-48" @input="updateLocalLanguage" />
</div>
<div class="w-full h-px bg-primary my-4" />
<div class="w-full h-px bg-white/10 my-4" />
<p v-if="!isGuest" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p>
<form v-if="!isGuest" @submit.prevent="submitChangePassword">
@ -42,7 +46,8 @@ export default {
password: null,
newPassword: null,
confirmPassword: null,
changingPassword: false
changingPassword: false,
selectedLanguage: ''
}
},
computed: {
@ -66,6 +71,9 @@ export default {
}
},
methods: {
updateLocalLanguage(lang) {
this.$setLanguageCode(lang)
},
logout() {
var rootSocket = this.$root.socket || {}
const logoutPayload = {
@ -113,6 +121,8 @@ export default {
})
}
},
mounted() {}
mounted() {
this.selectedLanguage = this.$languageCodes.current
}
}
</script>

View File

@ -79,7 +79,7 @@
<div class="py-2">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelLanguageDefaultServer }}</p>
<ui-dropdown v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-48" @input="updateServerLanguage" />
<ui-dropdown ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-48" @input="updateServerLanguage" />
</div>
</div>
@ -327,7 +327,6 @@ export default {
})
},
updateServerLanguage(val) {
this.$setLanguageCode(val)
this.updateSettingsKey('language', val)
},
updateSettingsKey(key, val) {
@ -343,6 +342,11 @@ export default {
console.log('Updated Server Settings', success)
this.updatingServerSettings = false
this.$toast.success('Server settings updated')
if (payload.language) {
// Updating language after save allows for re-rendering
this.$setLanguageCode(payload.language)
}
})
.catch((error) => {
console.error('Failed to update server settings', error)

View File

@ -1,15 +1,17 @@
import { getElementsByTagType } from "domutils"
import Vue from "vue"
import enUsStrings from '../strings/en-us.json'
import itStrings from '../strings/it.json'
import { supplant } from './utils'
const defaultCode = 'en-us'
const languageCodeMap = {
'en-us': 'English',
'es': 'Español',
'it': 'Italiano',
'pl': 'Polski',
'zh-cn': '汉语 (简化字)'
// 'es': 'Español',
// 'it': 'Italiano',
// 'pl': 'Polski',
'zh-cn': '简体中文 (Simplified Chinese)'
}
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
return {
@ -26,6 +28,7 @@ Vue.prototype.$languageCodes = {
}
Vue.prototype.$strings = { ...enUsStrings }
Vue.prototype.$getString = (key, subs) => {
if (!Vue.prototype.$strings[key]) return ''
if (subs && Array.isArray(subs) && subs.length) {
@ -71,6 +74,7 @@ async function loadi18n(code) {
}
console.log('i18n strings=', Vue.prototype.$strings)
Vue.prototype.$eventBus.$emit('change-lang', code)
return true
}
@ -98,7 +102,7 @@ async function initialize() {
if (!languageCodeMap[localLanguage]) {
console.warn('Invalid local language code', localLanguage)
localStorage.setItem('lang', defaultCode)
} else if (localLanguage !== defaultCode) {
} else {
Vue.prototype.$languageCodes.local = localLanguage
loadi18n(localLanguage)
}

View File

@ -1,54 +1,513 @@
{
"ButtonHome": "主页",
"ButtonLatest": "最新",
"ButtonLibrary": "图书馆",
"ButtonSeries": "系列",
"ButtonCollections": "收藏",
"ButtonAdd": "添加",
"ButtonAddChapters": "添加章节",
"ButtonAddPodcasts": "添加播客",
"ButtonAddYourFirstLibrary": "添加第一个图书库",
"ButtonApply": "应用",
"ButtonApplyChapters": "应用到章节",
"ButtonAuthors": "作者",
"ButtonSearch": "查找",
"ButtonBrowseForFolder": "浏览文件夹",
"ButtonCancel": "取消",
"ButtonCancelEncode": "取消编码",
"ButtonChangeRootPassword": "更改 Root 密码",
"ButtonCheckAndDownloadNewEpisodes": "检查并下载新剧集",
"ButtonChooseAFolder": "选择文件夹",
"ButtonChooseFiles": "选择文件",
"ButtonCloseFeed": "关闭源",
"ButtonCollections": "收藏",
"ButtonCreate": "创建",
"ButtonCreateBackup": "创建备份",
"ButtonDelete": "删除",
"ButtonEditChapters": "编辑章节",
"ButtonEditPodcast": "编辑播客",
"ButtonForceReScan": "强制重新扫描",
"ButtonFullPath": "完整路径",
"ButtonHide": "隐藏",
"ButtonHome": "首页",
"ButtonIssues": "反馈问题",
"ButtonChangePasswordSubmit": "提交",
"ButtonLatest": "最新",
"ButtonLogout": "注销",
"ButtonLookup": "查找",
"ButtonLibrary": "图书库",
"ButtonManageTracks": "管理音轨",
"ButtonMapChapterTitles": "章节标题结构",
"ButtonMatchAllAuthors": "匹配所有作者",
"ButtonMatchBooks": "匹配图书",
"ButtonNevermind": "没有关系",
"ButtonOk": "确定",
"ButtonOpenFeed": "打开源",
"ButtonOpenManager": "打开管理器",
"ButtonPlay": "播放",
"ButtonPlaying": "正在播放",
"ButtonPurgeAllCache": "清理所有缓存",
"ButtonPurgeItemsCache": "清理项目缓存",
"ButtonPurgeMediaProgress": "清理媒体进度",
"ButtonQuickMatch": "快速匹配",
"ButtonRead": "读取",
"ButtonRemove": "移除",
"ButtonRemoveAll": "移除所有",
"ButtonRemoveAllLibraryItems": "移除所有图书项目",
"ButtonRemoveFromContinueListening": "从继续收听中删除",
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
"ButtonReScan": "重新扫描",
"ButtonReset": "重置",
"ButtonRestore": "恢复",
"ButtonSave": "保存",
"ButtonSaveAndClose": "保存并关闭",
"ButtonSaveTracklist": "保存音轨列表",
"ButtonScan": "扫描",
"ButtonSearch": "查找",
"ButtonSelectFolderPath": "选择文件夹路径",
"ButtonSeries": "系列",
"ButtonShiftTimes": "快速移动时间",
"ButtonShow": "显示",
"ButtonStartM4BEncode": "开始 M4B 编码",
"ButtonStartMetadataEmbed": "开始嵌入元数据",
"ButtonSubmit": "提交",
"ButtonUpload": "上传",
"ButtonUploadBackup": "上传备份",
"ButtonUploadCover": "上传封面",
"ButtonUploadOPMLFile": "上传 OPML 文件",
"ButtonViewAll": "查看全部",
"ButtonYes": "确定",
"HeaderAccount": "帐户",
"HeaderChangePassword": "更改密码",
"HeaderSettings": "设置",
"HeaderLibraries": "图书馆",
"HeaderUsers": "用户",
"HeaderListeningSessions": "收听会话",
"HeaderAdvanced": "高级",
"HeaderAppriseNotificationSettings": "测试通知设置",
"HeaderAudiobookTools": "有声读物文件管理工具",
"HeaderAudioTracks": "音轨",
"HeaderBackups": "备份",
"HeaderChangePassword": "更改密码",
"HeaderChapters": "章节",
"HeaderChooseAFolder": "选择文件夹",
"HeaderCollection": "收藏",
"HeaderCollectionItems": "收藏项目",
"HeaderCover": "封面",
"HeaderDetails": "详情",
"HeaderEpisodes": "剧集",
"HeaderFiles": "文件",
"HeaderFindChapters": "查找章节",
"HeaderIgnoredFiles": "忽略的文件",
"HeaderItemFiles": "项目文件",
"HeaderLastListeningSession": "最后一次收听会话",
"HeaderLatestEpisodes": "最新剧集",
"HeaderLibraries": "图书库",
"HeaderLibraryFiles": "图书库文件",
"HeaderLibraryStats": "图书库统计数据",
"HeaderListeningSessions": "收听会话",
"HeaderListeningStats": "收听统计数据",
"HeaderLogin": "登录",
"HeaderLogs": "日志",
"HeaderMatch": "匹配",
"HeaderMetadataToEmbed": "嵌入元数据",
"HeaderNewAccount": "新建帐户",
"HeaderNewLibrary": "新建图书库",
"HeaderNotifications": "通知",
"HeaderLibraryStats": "图书馆统计数据",
"HeaderYourStats": "你的统计数据",
"HeaderSettingsGeneral": "一般",
"HeaderSettingsScanner": "扫描",
"HeaderOtherFiles": "其他文件",
"HeaderOpenRSSFeed": "打开 RSS 源",
"HeaderPermissions": "权限",
"HeaderPodcastsToAdd": "要添加的播客",
"HeaderPreviewCover": "预览封面",
"HeaderRemoveEpisode": "移除剧集",
"HeaderRemoveEpisodes": "移除 {0} 剧集",
"HeaderRSSFeedIsOpen": "RSS 源已打开",
"HeaderSavedMediaProgress": "保存媒体进度",
"HeaderSchedule": "计划任务",
"HeaderScheduleLibraryScans": "自动扫描图书库",
"HeaderSession": "会话",
"HeaderSetBackupSchedule": "设置备份计划任务",
"HeaderSettings": "设置",
"HeaderSettingsDisplay": "显示",
"HeaderSettingsExperimental": "实验功能",
"LabelUsername": "用户名",
"HeaderSettingsGeneral": "通用",
"HeaderSettingsScanner": "扫描",
"HeaderSleepTimer": "睡眠计时",
"HeaderStatsLongestItems": "项目时长(小时)",
"HeaderStatsMinutesListeningChart": "收听分钟数(最近7天)",
"HeaderStatsRecentSessions": "历史会话",
"HeaderStatsTop10Authors": "前 10 位作者",
"HeaderStatsTop5Genres": "前 5 种流派",
"HeaderTools": "工具",
"HeaderUpdateAccount": "更新帐户",
"HeaderUpdateAuthor": "更新作者",
"HeaderUpdateDetails": "更新详情",
"HeaderUpdateLibrary": "更新图书库",
"HeaderUsers": "用户",
"HeaderYourStats": "你的统计数据",
"LabelAccountType": "帐户类型",
"LabelPassword": "密码",
"LabelNewPassword": "新密码",
"LabelAccountTypeAdmin": "管理员",
"LabelAccountTypeGuest": "来宾",
"LabelAccountTypeUser": "用户",
"LabelActivity": "活动",
"LabelAddToCollection": "添加到收藏",
"LabelAddToCollectionBatch": "添加 {0} 图书到收藏",
"LabelAllUsers": "所有用户",
"LabelAuthor": "作者",
"LabelAuthors": "作者",
"LabelAutoDownloadEpisodes": "自动下载剧集",
"LabelBackToUser": "返回到用户",
"LabelBackupsEnableAutomaticBackups": "启用自动备份",
"LabelBackupsEnableAutomaticBackupsHelp": "备份保存到 /metadata/backups",
"LabelBackupsMaxBackupSize": "最大备份大小 (GB)",
"LabelBackupsMaxBackupSizeHelp": "为了防止错误配置, 如果备份超过配置的大小, 备份将失败.",
"LabelBackupsNumberToKeep": "要保留的备份个数",
"LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.",
"LabelBooks": "图书",
"LabelChangePassword": "修改密码",
"LabelChaptersFound": "找到的章节",
"LabelChapterTitle": "章节标题",
"LabelCollapseSeries": "折叠系列",
"LabelCollections": "收藏",
"LabelComplete": "完成",
"LabelConfirmPassword": "确认密码",
"LabelContinueListening": "继续收听",
"LabelContinueSeries": "继续收听系列",
"LabelCover": "封面",
"LabelCoverImageURL": "封面图像 URL",
"LabelCreatedAt": "创建时间",
"LabelCronExpression": "计划任务表达式",
"LabelCurrent": "当前",
"LabelCurrently": "当前:",
"LabelDatetime": "日期时间",
"LabelDescription": "描述",
"LabelDeselectAll": "全部取消选择",
"LabelDevice": "设备",
"LabelDeviceInfo": "设备信息",
"LabelDirectory": "目录",
"LabelDiscFromFilename": "从文件名获取光盘",
"LabelDiscFromMetadata": "从元数据获取光盘",
"LabelDownload": "下载",
"LabelDuration": "持续时间",
"LabelDurationFound": "找到持续时间:",
"LabelEdit": "编辑",
"LabelEnable": "启用",
"LabelEnd": "结束",
"LabelEpisode": "剧集",
"LabelEpisodeTitle": "剧集标题",
"LabelEpisodeType": "剧集类型",
"LabelExplicit": "显式",
"LabelFeedURL": "源 URL",
"LabelFile": "文件",
"LabelFilename": "文件名",
"LabelFilterByUser": "按用户筛选",
"LabelFindEpisodes": "查找剧集",
"LabelFinished": "听完",
"LabelFolder": "文件夹",
"LabelFolders": "文件夹",
"LabelGenres": "流派",
"LabelHardDeleteFile": "完全删除文件",
"LabelHour": "小时",
"LabelIcon": "图标",
"LabelIncludeInTracklist": "包含在音轨列表中",
"LabelIncomplete": "不完整",
"LabelInProgress": "正在进行",
"LabelInterval": "间隔",
"LabelInvalidParts": "无效部件",
"LabelItem": "项目",
"LabelLanguage": "语言",
"LabelLanguageDefaultServer": "默认服务器语言",
"LabelLastSeen": "上次查看时间",
"LabelLastTime": "最近一次",
"LabelLastUpdate": "最近更新",
"LabelLess": "较少",
"LabelLibrariesAccessibleToUser": "用户可访问的图书库",
"LabelLibrary": "图书库",
"LabelLibraryItem": "图书库项目",
"LabelLibraryName": "图书库名称",
"LabelLimit": "限度",
"LabelListenAgain": "再次收听",
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
"LabelMarkSeries": "标记系列",
"LabelMediaPlayer": "媒体播放器",
"LabelMediaType": "媒体类型",
"LabelMetadataProvider": "元数据提供者",
"LabelMetaTag": "元数据标签",
"LabelMinute": "分钟",
"LabelMissing": "丢失",
"LabelMissingParts": "丢失的部分",
"LabelMore": "更多",
"LabelName": "名称",
"LabelNarrators": "演播者",
"LabelNew": "新建",
"LabelNewestAuthors": "最新作者",
"LabelNewestEpisodes": "最新剧集",
"LabelNewPassword": "新密码",
"LabelNotes": "注意",
"LabelNotFinished": "未完成",
"LabelNotificationEvent": "通知事件",
"LabelNotificationAppriseURL": "通知 URL(s)",
"LabelNotificationAvailableVariables": "可用变量",
"LabelNotificationBodyTemplate": "正文模板",
"LabelNotificationTitleTemplate": "标题模板",
"LabelNotificationsMaxFailedAttempts": "最大失败尝试次数",
"LabelNotificationsMaxFailedAttemptsHelp": "如果多次发送失败,通知将被禁用",
"LabelNotificationsMaxQueueSize": "通知事件的最大队列大小",
"LabelNotificationsMaxQueueSizeHelp": "通知事件被限制为每秒触发 1 个. 如果队列处于最大大小, 则将忽略事件. 这可以防止通知垃圾邮件.",
"LabelOpenRSSFeed": "打开 RSS 源",
"LabelPassword": "密码",
"LabelPath": "路径",
"LabelPermissionsAccessAllLibraries": "可以访问所有图书库",
"LabelPermissionsAccessAllTags": "可以访问所有标签",
"LabelPermissionsAccessExplicitContent": "可以访问显式内容",
"LabelPermissionsDelete": "可以删除",
"LabelPermissionsDownload": "可以下载",
"LabelPermissionsUpdate": "可以更新",
"LabelPermissionsUpload": "可以上传",
"LabelPhotoPathURL": "图片路径或 URL",
"LabelPlayMethod": "播放方法",
"LabelPodcast": "播客",
"LabelPodcasts": "播客",
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
"LabelProgress": "进度",
"LabelProvider": "供应商",
"LabelPubDate": "出版日期",
"LabelPublisher": "出版商",
"LabelPublishYear": "发布年份",
"LabelRecentlyAdded": "最近添加",
"LabelRecentSeries": "最近添加系列",
"LabelRegion": "区域",
"LabelReleaseDate": "发布日期",
"LabelRSSFeedSlug": "RSS 源段",
"LabelRSSFeedURL": "RSS 源 URL",
"LabelSearchTerm": "搜索项",
"LabelSearchTitle": "搜索标题",
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
"LabelSeason": "季",
"LabelSequence": "序列",
"LabelSeries": "系列",
"LabelSeriesName": "系列名称",
"LabelSettingsBookshelfViewHelp": "带有木架子的拟物化设计",
"LabelSettingsChromecastSupport": "Chromecast 支持",
"LabelSettingsDateFormat": "日期格式",
"LabelSettingsDisableWatcher": "禁用监视程序",
"LabelSettingsDisableWatcherForLibrary": "禁用图书库的文件夹监视程序",
"LabelSettingsDisableWatcherHelp": "检测到文件更改时禁用自动添加和更新项目. *需要重启服务器",
"LabelSettingsEnableEReader": "为所有用户启用电子阅读器",
"LabelSettingsEnableEReaderHelp": "电子阅读器仍在开发中,但可以使用此设置向所有用户打开它(或使用 \"实验功能\" 切换仅供你使用)",
"LabelSettingsExperimentalFeatures": "实验功能",
"LabelSettingsExperimentalFeaturesHelp": "开发中的功能需要你的反馈并帮助测试. 点击打开 github 讨论.",
"LabelSettingsFindCovers": "查找封面",
"LabelSettingsFindCoversHelp": "如果你的有声读物在文件夹中没有嵌入封面或封面图像, 扫描将尝试查找封面.<br>注意: 这将延长扫描时间",
"LabelSettingsHomePageBookshelfView": "首页使用书架视图",
"LabelSettingsLibraryBookshelfView": "图书库使用书架视图",
"LabelSettingsOverdriveMediaMarkers": "对章节使用 Overdrive 媒体标记",
"LabelSettingsOverdriveMediaMarkersHelp": "Overdrive 的 MP3 文件带有作为自定义元数据嵌入的章节时间. 启用此功能将自动将这些标签用于章节计时",
"LabelSettingsParseSubtitles": "解析副标题",
"LabelSettingsParseSubtitlesHelp": "从有声读物文件夹中提取副标题.<br>副标题必须用 \" - \" 分隔.<br>例: \"书名 - 这里是副标题\" 则显示副标题 \"这里是副标题\"",
"LabelSettingsPreferAudioMetadata": "首选音频元数据",
"LabelSettingsPreferAudioMetadataHelp": "音频文件 ID3 元标记将用于文件夹名称上图书的详细信息",
"LabelSettingsPreferMatchedMetadata": "首选匹配的元数据",
"LabelSettingsPreferMatchedMetadataHelp": "使用快速匹配时, 匹配的数据将覆盖项目详细信息. 默认情况下, 快速匹配将只填充缺少的详细信息.",
"LabelSettingsPreferOPFMetadata": "首选 OPF 元数据",
"LabelSettingsPreferOPFMetadataHelp": "OPF 文件元数据将用于文件夹名称上图书的详细信息",
"LabelSettingsSkipMatchingBooksWithASIN": "跳过匹配已有 ASIN 的图书",
"LabelSettingsSkipMatchingBooksWithISBN": "跳过匹配已有 ISBN 的图书",
"LabelSettingsSortingIgnorePrefixes": "排序时忽略前缀",
"LabelSettingsSortingIgnorePrefixesHelp": "例如: 前缀为 \"The\" 的图书标题 \"The Book Title\" 将按 \"Book Title, The\" 进行排序",
"LabelSettingsStoreCoversWithItem": "存储项目封面",
"LabelSettingsStoreCoversWithItemHelp": "默认情况下封面存储在/metadata/items文件夹中, 启用此设置将存储封面在你图书项目文件夹中. 只保留一个名为 \"cover\" 的文件",
"LabelSettingsStoreMetadataWithItem": "存储项目元数据",
"LabelSettingsStoreMetadataWithItemHelp": "默认情况下元数据文件存储在/metadata/items文件夹中, 启用此设置将存储元数据在你图书项目文件夹中. 使 .abs 文件护展名",
"LabelSettingsSortingIgnorePrefixes": "排序时忽略前缀",
"LabelSettingsSortingIgnorePrefixesHelp": "例: 前缀 \"这本\" 书名 \"这本书名\" 将按 \"书名, 这本\" 排序",
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
"LabelSettingsChromecastSupport": "Chromecast 支持",
"LabelSettingsHomePageBookshelfView": "主页使用书架视图",
"LabelSettingsLibraryBookshelfView": "图书馆使用书架视图",
"LabelSettingsBookshelfViewHelp": "带有木架子的拟物化设计",
"LabelSettingsDateFormat": "日期格式",
"LabelSettingsParseSubtitles": "解析字幕",
"LabelSettingsParseSubtitlesHelp": "从有声读物文件夹中提取字幕.<br>字幕必须用 \" - \" 分隔.<br>例: \"书名 - 字幕\" 则显示字幕 \"这里有字幕\"",
"LabelSettingsFindCovers": "查找封面",
"LabelSettingsFindCoversHelp": "如果您的有声读物在文件夹中没有嵌入封面或封面图像, 扫描时将尝试查找封面.<br>注意: 这将延长扫描时间",
"MessageReportBugsAndContribute": "报告错误, 请求新功能, 做贡献请点击",
"NoteChangeRootPassword": "Root 用户是唯一可以有空密码的用户",
"SearchPlaceholder": "搜索.."
}
"LabelSettingsSquareBookCovers": "用户方形图书封面",
"LabelSettingsSquareBookCoversHelp": "比起标准的 1.6:1 图书封面,更喜欢使用方形封面",
"LabelShowAll": "全部显示",
"LabelSize": "大小",
"LabelStart": "开始",
"LabelStarted": "开始于",
"LabelStartedAt": "从这开始",
"LabelStartTime": "开始时间",
"LabelStatsAudioTracks": "音轨",
"LabelStatsAuthors": "作者",
"LabelStatsBestDay": "最好的一天",
"LabelStatsDailyAverage": "每日平均值",
"LabelStatsDays": "天",
"LabelStatsDaysListened": "收听天数",
"LabelStatsHours": "小时",
"LabelStatsInARow": "在一行",
"LabelStatsItemsFinished": "已完成的项目",
"LabelStatsItemsInLibrary": "图书库中的项目",
"LabelStatsMinutes": "分钟",
"LabelStatsMinutesListening": "收听分钟数",
"LabelStatsOverall": "总计",
"LabelStatsWeekListening": "每周收听",
"LabelSubtitle": "副标题",
"LabelSupportedFileTypes": "支持的文件类型",
"LabelTags": "标签",
"LabelTagsAccessibleToUser": "用户可访问的标签",
"LabelTimeListened": "收听时间",
"LabelTimeListenedToday": "今日收听的时间",
"LabelTimeRemaining": "剩余 {0}",
"LabelTimeToShift": "快速移动时间以秒为单位",
"LabelTitle": "标题",
"LabelTotalTimeListened": "总收听时间",
"LabelTrackFromFilename": "从文件名获取音轨",
"LabelTrackFromMetadata": "从源数据获取音轨",
"LabelType": "类型",
"LabelUnknown": "未知",
"LabelUpdateCover": "更新封面",
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
"LabelUpdatedAt": "更新时间",
"LabelUpdateDetails": "更新详细信息",
"LabelUpdateDetailsHelp": "找到匹配项时允许覆盖所选书籍存在的详细信息",
"LabelUploaderDragAndDrop": "拖放文件或文件夹",
"LabelUploaderDropFiles": "删除文件",
"LabelUseChapterTrack": "使用章节音轨",
"LabelUseFullTrack": "使用完整音轨",
"LabelUser": "用户",
"LabelUsername": "用户名",
"LabelValue": "值",
"LabelVersion": "版本",
"LabelWeekdaysToRun": "工作日运行",
"LabelYourAudiobookDuration": "你的有声读物持续时间",
"LabelYourBookmarks": "你的书签",
"LabelYourProgress": "你的进度",
"MessageBackupsDescription": "备份包括用户, 用户进度, 图书库项目详细信息, 服务器设置和图像, 存储在",
"MessageBackupsNote": "备份不包括存储在您的图书库文件夹中的任何文件.",
"MessageBatchQuickMatchDescription": "快速匹配将尝试为所选项目添加缺少的封面和元数据. 启用以下选项以允许快速匹配覆盖现有封面和或元数据.",
"MessageChapterEndIsAfter": "章节结束是在有声读物结束之后",
"MessageChapterStartIsAfter": "章节开始是在有声读物结束之后",
"MessageCheckingCron": "检查计划任务...",
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
"MessageConfirmDeleteLibrary": "你确定要永久删除图书库 \"{0}\"?",
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
"MessageDownloadingEpisode": "正在下载剧集",
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
"MessageEmbedFinished": "嵌入完成!",
"MessageEpisodesQueuedForDownload": "{0} 个剧集排队等待下载",
"MessageFeedURLWillBe": "源 URL 将改为 {0}",
"MessageFetching": "正在获取...",
"MessageForceReScanDescription": "将像重新扫描一样再次扫描所有文件. 音频文件 ID3 标签, OPF 文件和文本文件将被扫描为新文件.",
"MessageImportantNotice": "重要通知!",
"MessageInsertChapterBelow": "在下面插入章节",
"MessageItemsSelected": "已选定 {0} 个项目",
"MessageJoinUsOn": "加入我们",
"MessageListeningSessionsInTheLastYear": "去年收听 {0} 个会话",
"MessageLoading": "加载...",
"MessageLoadingFolders": "加载文件夹...",
"MessageM4BFailed": "M4B 失败!",
"MessageM4BFinished": "M4B 完成!",
"MessageMapChapterTitles": "将章节标题映射到现有的有声读物章节, 无需调整时间戳",
"MessageMarkAsFinished": "标记为已听完",
"MessageMarkAsNotFinished": "标记为未听完",
"MessageMatchBooksDescription": "尝试将图书库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.",
"MessageNoAudioTracks": "没有音轨",
"MessageNoAuthors": "没有作者",
"MessageNoBackups": "没有备份",
"MessageNoBookmarks": "没有书签",
"MessageNoChapters": "没有章节",
"MessageNoCollections": "没有收藏",
"MessageNoCoversFound": "没有找到封面",
"MessageNoDescription": "没有描述",
"MessageNoEpisodeMatchesFound": "没有找到任何剧集匹配项",
"MessageNoEpisodes": "没有剧集",
"MessageNoFoldersAvailable": "没有可用文件夹",
"MessageNoGenres": "无流派",
"MessageNoItems": "无项目",
"MessageNoItemsFound": "未找到任何项目",
"MessageNoListeningSessions": "无收听会话",
"MessageNoLogs": "无日志",
"MessageNoMediaProgress": "无媒体进度",
"MessageNoNotifications": "无通知",
"MessageNoPodcastsFound": "未找到播客",
"MessageNoResults": "无结果",
"MessageNoSearchResultsFor": "没有搜索到结果 \"{0}\"",
"MessageNoUpdateNecessary": "无需更新",
"MessageNoUpdatesWereNecessary": "无需更新",
"MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url",
"MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.",
"MessageRemoveAllItemsWarning": "警告! 此操作将从数据库中删除所有的图书库项, 包括您所做的任何更新或匹配. 这不会对实际文件产生任何影响. 你确定吗?",
"MessageRemoveEpisodes": "移除 {0} 剧集",
"MessageRemoveUserWarning": "是否确实要永久删除用户 \"{0}\"?",
"MessageReportBugsAndContribute": "报告错误、请求功能和贡献在",
"MessageRestoreBackupConfirm": "您确定要恢复创建的这个备份",
"MessageRestoreBackupWarning": "恢复备份将覆盖位于 /config 的整个数据库并覆盖 /metadata/items & /metadata/authors 中的图像.<br /><br />备份不会修改图书库文件夹中的任何文件. 如果您已启用服务器设置将封面和元数据存储在库文件夹中,则不会备份或覆盖这些内容.<br /><br />将自动刷新使用服务器的所有客户端.",
"MessageSearchResultsFor": "搜索结果",
"MessageServerCouldNotBeReached": "无法访问服务器",
"MessageStartPlaybackAtTime": "开始播放 \"{0}\" 在 {1}?",
"MessageThinking": "思考...",
"MessageUploaderItemFailed": "上传失败",
"MessageUploaderItemSuccess": "上传成功!",
"MessageUploading": "正在上传...",
"MessageValidCronExpression": "有效的计划任务表达式",
"MessageWatcherIsDisabledGlobally": "在服务器设置中禁用全局监视程序",
"MessageYourAudiobookDurationIsLonger": "您的有声读物持续时间比找到的持续时间长",
"MessageYourAudiobookDurationIsShorter": "您的有声读物持续时间比找到的持续时间短",
"NoteChangeRootPassword": "Root 是唯一可以拥有空密码的用户",
"NoteChapterEditorTimes": "注意: 第一章开始时间必须保持在 0:00, 最后一章开始时间不能超过有声读物持续时间.",
"NoteFolderPicker": "注意: 将不显示已映射的文件夹",
"NoteFolderPickerDebian": "注意: debian 安装的文件夹选择器尚未完全实现. 您应该直接输入图书库的路径.",
"NoteRSSFeedPodcastAppsHttps": "警告: 大多数播客应用程序都需要 RSS 源 URL 使用 HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "警告: 您的一集或多集没有发布日期. 一些播客应用程序要求这样做.",
"NoteUploaderFoldersWithMediaFiles": "包含媒体文件的文件夹将作为单独的图书库项目处理.",
"NoteUploaderOnlyAudioFiles": "如果只上传音频文件, 则每个音频文件将作为单独的有声读物处理.",
"NoteUploaderUnsupportedFiles": "不支持的文件将被忽略. 选择或删除文件夹时, 将忽略不在项目文件夹中的其他文件.",
"PlaceholderNewCollection": "新建收藏夹名称",
"PlaceholderNewFolderPath": "输入文件夹路径",
"PlaceholderSearch": "查找..",
"ToastAccountUpdateFailed": "账户更新失败",
"ToastAccountUpdateSuccess": "帐户已更新",
"ToastAuthorImageRemoveFailed": "作者图像删除失败",
"ToastAuthorImageRemoveSuccess": "作者图像已删除",
"ToastAuthorUpdateFailed": "作者更新失败",
"ToastAuthorUpdateMerged": "作者已合并",
"ToastAuthorUpdateSuccess": "作者已更新",
"ToastAuthorUpdateSuccessNoImageFound": "作者已更新 (未找到图像)",
"ToastBackupCreateFailed": "备份创建失败",
"ToastBackupCreateSuccess": "备份已创建",
"ToastBackupDeleteFailed": "备份删除失败",
"ToastBackupDeleteSuccess": "备份已删除",
"ToastBackupRestoreFailed": "备份还原失败",
"ToastBackupUploadFailed": "上传备份失败",
"ToastBackupUploadSuccess": "备份已上传",
"ToastBatchUpdateFailed": "批量更新失败",
"ToastBatchUpdateSuccess": "批量更新成功",
"ToastBookmarkCreateFailed": "创建书签失败",
"ToastBookmarkCreateSuccess": "书签已添加",
"ToastBookmarkRemoveFailed": "书签删除失败",
"ToastBookmarkRemoveSuccess": "书签已删除",
"ToastBookmarkUpdateFailed": "书签更新失败",
"ToastBookmarkUpdateSuccess": "书签已更新",
"ToastCollectionItemsRemoveFailed": "从收藏夹移除项目失败",
"ToastCollectionItemsRemoveSuccess": "项目从收藏夹移除",
"ToastCollectionRemoveFailed": "删除收藏夹失败",
"ToastCollectionRemoveSuccess": "收藏夹已删除",
"ToastCollectionUpdateFailed": "更新收藏夹失败",
"ToastCollectionUpdateSuccess": "收藏夹已更新",
"ToastItemCoverUpdateFailed": "更新项目封面失败",
"ToastItemCoverUpdateSuccess": "项目封面已更新",
"ToastItemDetailsUpdateFailed": "更新项目详细信息失败",
"ToastItemDetailsUpdateSuccess": "项目详细信息已更新",
"ToastItemDetailsUpdateUnneeded": "项目详细信息无需更新",
"ToastItemMarkedAsFinishedFailed": "标记为听完失败",
"ToastItemMarkedAsFinishedSuccess": "标记为听完的项目",
"ToastItemMarkedAsNotFinishedFailed": "标记为未听完失败",
"ToastItemMarkedAsNotFinishedSuccess": "标记为未听完的项目",
"ToastLibraryCreateFailed": "创建图书库失败",
"ToastLibraryCreateSuccess": "图书库 \"{0}\" 创建成功",
"ToastLibraryDeleteFailed": "删除图书库失败",
"ToastLibraryDeleteSuccess": "图书库已删除",
"ToastLibraryScanFailedToStart": "无法启动扫描",
"ToastLibraryScanStarted": "图书库扫描已启动",
"ToastLibraryUpdateFailed": "更新图书库失败",
"ToastLibraryUpdateSuccess": "图书库 \"{0}\" 已更新",
"ToastPodcastCreateFailed": "创建播客失败",
"ToastPodcastCreateSuccess": "已成功创建播客",
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
"ToastRemoveItemFromCollectionFailed": "从收藏中删除项目失败",
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
"ToastSessionDeleteFailed": "删除会话失败",
"ToastSessionDeleteSuccess": "会话已删除",
"ToastUserDeleteFailed": "删除用户失败",
"ToastUserDeleteSuccess": "用户已删除",
"WeekdayFriday": "星期五",
"WeekdayMonday": "星期一",
"WeekdaySaturday": "星期六",
"WeekdaySunday": "星期日",
"WeekdayThursday": "星期四",
"WeekdayTuesday": "星期二",
"WeekdayWednesday": "星期三"
}

View File

@ -83,7 +83,7 @@ class Server {
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
// Routers
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.notificationManager, this.taskManager, this.emitter.bind(this), this.clientEmitter.bind(this))
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.notificationManager, this.taskManager, this.getUsersOnline.bind(this), this.emitter.bind(this), this.clientEmitter.bind(this))
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
this.staticRouter = new StaticRouter(this.db)
@ -95,8 +95,7 @@ class Server {
this.clients = {}
}
get usersOnline() {
// TODO: Map open user sessions
getUsersOnline() {
return Object.values(this.clients).filter(c => c.user).map(client => {
return client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems)
})
@ -481,7 +480,7 @@ class Server {
backups: (this.backupManager.backups || []).map(b => b.toJSON())
}
if (user.type === 'root') {
initialPayload.usersOnline = this.usersOnline
initialPayload.usersOnline = this.getUsersOnline()
}
client.socket.emit('init', initialPayload)
}

View File

@ -183,6 +183,19 @@ class UserController {
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
}
// POST: api/users/online (admin)
async getOnlineUsers(req, res) {
if (!req.user.isAdminOrUp) {
return res.sendStatus(404)
}
const usersOnline = this.getUsersOnline()
res.json({
usersOnline,
openSessions: this.playbackSessionManager.sessions
})
}
middleware(req, res, next) {
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
return res.sendStatus(403)

View File

@ -246,6 +246,7 @@ class LibraryItem {
setLastScan() {
this.lastScan = Date.now()
this.updatedAt = Date.now()
this.scanVersion = version
}

View File

@ -26,7 +26,7 @@ const Series = require('../objects/entities/Series')
const FileSystemController = require('../controllers/FileSystemController')
class ApiRouter {
constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, notificationManager, taskManager, emitter, clientEmitter) {
constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, notificationManager, taskManager, getUsersOnline, emitter, clientEmitter) {
this.db = db
this.auth = auth
this.scanner = scanner
@ -42,6 +42,7 @@ class ApiRouter {
this.cronManager = cronManager
this.notificationManager = notificationManager
this.taskManager = taskManager
this.getUsersOnline = getUsersOnline
this.emitter = emitter
this.clientEmitter = clientEmitter
@ -113,6 +114,7 @@ class ApiRouter {
//
this.router.post('/users', UserController.middleware.bind(this), UserController.create.bind(this))
this.router.get('/users', UserController.middleware.bind(this), UserController.findAll.bind(this))
this.router.get('/users/online', UserController.getOnlineUsers.bind(this))
this.router.get('/users/:id', UserController.middleware.bind(this), UserController.findOne.bind(this))
this.router.patch('/users/:id', UserController.middleware.bind(this), UserController.update.bind(this))
this.router.delete('/users/:id', UserController.middleware.bind(this), UserController.delete.bind(this))

View File

@ -776,9 +776,14 @@ class Scanner {
for (const key in matchDataTransformed) {
if (matchDataTransformed[key]) {
if (key === 'genres') {
if ((!libraryItem.media.metadata.genres || options.overrideDetails)) {
// TODO: Genres array or string?
updatePayload.metadata[key] = matchDataTransformed[key].split(',').map(v => v.trim()).filter(v => !!v)
if ((!libraryItem.media.metadata.genres.length || options.overrideDetails)) {
var genresArray = []
if (Array.isArray(matchDataTransformed[key])) genresArray = [...matchDataTransformed[key]]
else { // Genres should always be passed in as an array but just incase handle a string
Logger.warn(`[Scanner] quickMatch genres is not an array ${matchDataTransformed[key]}`)
genresArray = matchDataTransformed[key].split(',').map(v => v.trim()).filter(v => !!v)
}
updatePayload.metadata[key] = genresArray
}
} else if (libraryItem.media.metadata[key] !== matchDataTransformed[key] && (!libraryItem.media.metadata[key] || options.overrideDetails)) {
updatePayload.metadata[key] = matchDataTransformed[key]