mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-06-23 01:18:59 +02:00
feat: add podcastFilenameFormat
This commit is contained in:
parent
5e5a988f7a
commit
8d988cf353
@ -35,6 +35,9 @@
|
||||
<div class="md:w-1/4 p-2">
|
||||
<ui-text-input-with-label v-model="podcast.language" :label="$strings.LabelLanguage" />
|
||||
</div>
|
||||
<div class="md:w-1/4 p-2">
|
||||
<ui-text-input-with-label v-model="podcast.podcastFilenameFormat" :label="$strings.PodcastFilenameFormatHelp" />
|
||||
</div>
|
||||
<div class="md:w-1/4 px-2 pt-7">
|
||||
<ui-checkbox v-model="podcast.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||
</div>
|
||||
@ -95,6 +98,7 @@ export default {
|
||||
itunesArtistId: '',
|
||||
autoDownloadEpisodes: false,
|
||||
language: '',
|
||||
filenameFormat: '',
|
||||
explicit: false,
|
||||
type: ''
|
||||
}
|
||||
@ -193,6 +197,7 @@ export default {
|
||||
itunesId: this.podcast.itunesId,
|
||||
itunesArtistId: this.podcast.itunesArtistId,
|
||||
language: this.podcast.language,
|
||||
podcastFilenameFormat: this.podcast.podcastFilenameFormat,
|
||||
explicit: this.podcast.explicit,
|
||||
type: this.podcast.type
|
||||
},
|
||||
@ -230,6 +235,7 @@ export default {
|
||||
this.podcast.itunesId = this._podcastData.id || ''
|
||||
this.podcast.itunesArtistId = this._podcastData.artistId || ''
|
||||
this.podcast.language = this._podcastData.language || this.feedMetadata.language || ''
|
||||
this.podcast.filenameFormat = this._podcastData.filenameFormat || '%T'
|
||||
this.podcast.autoDownloadEpisodes = false
|
||||
this.podcast.type = this._podcastData.type || this.feedMetadata.type || 'episodic'
|
||||
|
||||
|
@ -43,6 +43,16 @@
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" @input="handleInputChange" />
|
||||
</div>
|
||||
<div class="w-1/4 px-1">
|
||||
<ui-text-input-with-label ref="podcastFilenameFormatInput" v-model="details.podcastFilenameFormat" :label="$strings.PodcastFilenameFormatHelp" trim-whitespace @input="handleInputChange" >
|
||||
<div class="flex -mb-0.5">
|
||||
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.PodcastFilenameFormatHelp }}</p>
|
||||
<ui-tooltip direction="top" :text="$strings.PodcastFilenameFormatHelpContent">
|
||||
<span class="material-symbols text-base">info</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</ui-text-input-with-label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -71,6 +81,7 @@ export default {
|
||||
itunesArtistId: null,
|
||||
explicit: false,
|
||||
language: null,
|
||||
podcastFilenameFormat: null,
|
||||
type: null
|
||||
},
|
||||
newTags: []
|
||||
@ -158,6 +169,7 @@ export default {
|
||||
if (this.$refs.feedUrlInput) this.$refs.feedUrlInput.blur()
|
||||
if (this.$refs.itunesIdInput) this.$refs.itunesIdInput.blur()
|
||||
if (this.$refs.languageInput) this.$refs.languageInput.blur()
|
||||
if (this.$refs.podcastFilenameFormatInput) this.$refs.podcastFilenameFormatInput.blur()
|
||||
|
||||
if (this.$refs.genresSelect && this.$refs.genresSelect.isFocused) {
|
||||
this.$refs.genresSelect.forceBlur()
|
||||
@ -240,6 +252,7 @@ export default {
|
||||
this.details.itunesId = this.mediaMetadata.itunesId || ''
|
||||
this.details.itunesArtistId = this.mediaMetadata.itunesArtistId || ''
|
||||
this.details.language = this.mediaMetadata.language || ''
|
||||
this.details.podcastFilenameFormat = this.mediaMetadata.podcastFilenameFormat || '%T'
|
||||
this.details.explicit = !!this.mediaMetadata.explicit
|
||||
this.details.type = this.mediaMetadata.type || 'episodic'
|
||||
|
||||
|
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.23.0",
|
||||
"version": "2.23.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.23.0",
|
||||
"version": "2.23.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.23.0",
|
||||
"version": "2.23.1",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
@ -924,6 +924,8 @@
|
||||
"PlaceholderNewPlaylist": "New playlist name",
|
||||
"PlaceholderSearch": "Search..",
|
||||
"PlaceholderSearchEpisode": "Search episode..",
|
||||
"PodcastFilenameFormatHelpContent": "%S for Season, %E for Episode, %Y for Year, %M for Month, %D for Day and %T for title",
|
||||
"PodcastFilenameFormatHelp": "Podcast filename format",
|
||||
"StatsAuthorsAdded": "authors added",
|
||||
"StatsBooksAdded": "books added",
|
||||
"StatsBooksAdditional": "Some additions include…",
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.23.0",
|
||||
"version": "2.23.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.23.0",
|
||||
"version": "2.23.1",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.23.0",
|
||||
"version": "2.23.1",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
|
@ -95,7 +95,7 @@ class LibraryController {
|
||||
return res.status(400).send('Invalid request. Settings "metadataPrecedence" must be an array')
|
||||
}
|
||||
newLibraryPayload.settings[key] = [...req.body.settings[key]]
|
||||
} else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion') {
|
||||
} else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion' || key === 'podcastFilenameFormat') {
|
||||
if (!req.body.settings[key]) continue
|
||||
if (typeof req.body.settings[key] !== 'string') {
|
||||
return res.status(400).send(`Invalid request. Settings "${key}" must be a string`)
|
||||
@ -318,7 +318,7 @@ class LibraryController {
|
||||
updatedSettings[key] = [...req.body.settings[key]]
|
||||
Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`)
|
||||
}
|
||||
} else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion') {
|
||||
} else if (key === 'autoScanCronExpression' || key === 'podcastSearchRegion' || key === 'podcastFilenameFormat' ) {
|
||||
if (req.body.settings[key] !== null && typeof req.body.settings[key] !== 'string') {
|
||||
Logger.error(`[LibraryController] Invalid request. Settings "${key}" must be a string`)
|
||||
return res.status(400).send(`Invalid request. Settings "${key}" must be a string`)
|
||||
|
@ -622,6 +622,7 @@ class PodcastManager {
|
||||
itunesId: '',
|
||||
itunesArtistId: '',
|
||||
language: '',
|
||||
podcastFilenameFormat: '',
|
||||
numEpisodes: feed.numEpisodes
|
||||
}
|
||||
}
|
||||
|
64
server/migrations/v2.23.1-add-podcastFilenameFormat.js
Normal file
64
server/migrations/v2.23.1-add-podcastFilenameFormat.js
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @typedef MigrationContext
|
||||
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
|
||||
* @property {import('../Logger')} logger - a Logger object.
|
||||
*
|
||||
* @typedef MigrationOptions
|
||||
* @property {MigrationContext} context - an object containing the migration context.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This upward migration script adds a new variable to allow saving podcast filename formats
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
const migrationVersion = '2.23.1'
|
||||
const migrationName = `${migrationVersion}-add-podcastFilenameFormat-to-podcasts`
|
||||
const loggerPrefix = `[${migrationVersion} migration]`
|
||||
async function up({ context: { queryInterface, logger } }) {
|
||||
// Upwards migration script
|
||||
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
// Run reindex nocase to fix potential corruption issues due to the bad sqlite extension introduced in v2.12.0
|
||||
logger.info(`${loggerPrefix} adding the variable`)
|
||||
await addColumn(queryInterface, logger, 'podcasts', 'podcastFilenameFormat', { type: queryInterface.sequelize.Sequelize.STRING, allowNull: true })
|
||||
|
||||
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This downward migration script is a no-op.
|
||||
*
|
||||
* @param {MigrationOptions} options - an object containing the migration context.
|
||||
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
|
||||
*/
|
||||
async function down({ context: { queryInterface, logger } }) {
|
||||
// Downward migration script
|
||||
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
|
||||
|
||||
logger.info(`${loggerPrefix} Dropping column from table`)
|
||||
await removeColumn(queryInterface, logger, 'podcasts', 'podcastFilenameFormat')
|
||||
|
||||
|
||||
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
|
||||
}
|
||||
|
||||
async function addColumn(queryInterface, logger, table, column, options) {
|
||||
logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
|
||||
const tableDescription = await queryInterface.describeTable(table)
|
||||
if (!tableDescription[column]) {
|
||||
await queryInterface.addColumn(table, column, options)
|
||||
logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
|
||||
} else {
|
||||
logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeColumn(queryInterface, logger, table, column) {
|
||||
logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
|
||||
await queryInterface.removeColumn(table, column)
|
||||
logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
|
||||
}
|
||||
|
||||
module.exports = { up, down }
|
@ -39,6 +39,8 @@ class Podcast extends Model {
|
||||
/** @type {string} */
|
||||
this.language
|
||||
/** @type {string} */
|
||||
this.podcastFilenameFormat
|
||||
/** @type {string} */
|
||||
this.podcastType
|
||||
/** @type {boolean} */
|
||||
this.explicit
|
||||
@ -94,6 +96,7 @@ class Podcast extends Model {
|
||||
itunesId: typeof payload.metadata.itunesId === 'string' ? payload.metadata.itunesId : null,
|
||||
itunesArtistId: typeof payload.metadata.itunesArtistId === 'string' ? payload.metadata.itunesArtistId : null,
|
||||
language: typeof payload.metadata.language === 'string' ? payload.metadata.language : null,
|
||||
podcastFilenameFormat: typeof payload.metadata.podcastFilenameFormat === 'string' ? payload.metadata.podcastFilenameFormat : null,
|
||||
podcastType: typeof payload.metadata.type === 'string' ? payload.metadata.type : null,
|
||||
explicit: !!payload.metadata.explicit,
|
||||
autoDownloadEpisodes: !!payload.autoDownloadEpisodes,
|
||||
@ -131,6 +134,7 @@ class Podcast extends Model {
|
||||
itunesId: DataTypes.STRING,
|
||||
itunesArtistId: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
podcastFilenameFormat: DataTypes.STRING,
|
||||
podcastType: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
|
||||
@ -186,6 +190,7 @@ class Podcast extends Model {
|
||||
itunesId: this.itunesId,
|
||||
itunesArtistId: this.itunesArtistId,
|
||||
language: this.language,
|
||||
podcastFilenameFormat: this.podcastFilenameFormat,
|
||||
explicit: !!this.explicit,
|
||||
podcastType: this.podcastType
|
||||
}
|
||||
@ -202,7 +207,7 @@ class Podcast extends Model {
|
||||
let hasUpdates = false
|
||||
|
||||
if (payload.metadata) {
|
||||
const stringKeys = ['title', 'author', 'releaseDate', 'feedUrl', 'imageUrl', 'description', 'itunesPageUrl', 'itunesId', 'itunesArtistId', 'language', 'type']
|
||||
const stringKeys = ['title', 'author', 'releaseDate', 'feedUrl', 'imageUrl', 'description', 'itunesPageUrl', 'itunesId', 'itunesArtistId', 'language', 'type','podcastFilenameFormat']
|
||||
stringKeys.forEach((key) => {
|
||||
let newKey = key
|
||||
if (key === 'type') {
|
||||
@ -386,6 +391,7 @@ class Podcast extends Model {
|
||||
itunesArtistId: this.itunesArtistId,
|
||||
explicit: this.explicit,
|
||||
language: this.language,
|
||||
podcastFilenameFormat: this.podcastFilenameFormat,
|
||||
type: this.podcastType
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ class PodcastEpisodeDownload {
|
||||
this.appendRandomId = false
|
||||
|
||||
this.targetFilename = null
|
||||
this.podcastFilenameFormat = null
|
||||
|
||||
this.startedAt = null
|
||||
this.createdAt = null
|
||||
@ -89,13 +90,38 @@ class PodcastEpisodeDownload {
|
||||
if (!this.rssPodcastEpisode.publishedAt) return null
|
||||
return new Date(this.rssPodcastEpisode.publishedAt).getFullYear()
|
||||
}
|
||||
formatFilename() {
|
||||
const {publishedAt, season, episode, title} = this.rssPodcastEpisode;
|
||||
const fileformat = this.libraryItem.media.podcastFilenameFormat || '%T'
|
||||
let filename = fileformat
|
||||
if (publishedAt) {
|
||||
const dt = new Date(publishedAt)
|
||||
const year = dt.getFullYear()
|
||||
const month = String(dt.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(dt.getDate()).padStart(2,'0')
|
||||
filename = filename
|
||||
.replace("%Y",year)
|
||||
.replace("%M",month)
|
||||
.replace("%D",day)
|
||||
}
|
||||
if (season) {
|
||||
filename = filename.replace("%S",season)
|
||||
}
|
||||
if (episode){
|
||||
filename = filename.replace("%E",episode)
|
||||
}
|
||||
if (title){
|
||||
filename = filename.replace("%T",title.trim() || '')
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} title
|
||||
*/
|
||||
getSanitizedFilename(title) {
|
||||
getSanitizedFilename() {
|
||||
const appendage = this.appendRandomId ? ` (${this.id})` : ''
|
||||
const filename = `${title.trim()}${appendage}.${this.fileExtension}`
|
||||
const filename = `${this.formatFilename()}${appendage}.${this.fileExtension}`
|
||||
return sanitizeFilename(filename)
|
||||
}
|
||||
|
||||
@ -104,7 +130,7 @@ class PodcastEpisodeDownload {
|
||||
*/
|
||||
setAppendRandomId(appendRandomId) {
|
||||
this.appendRandomId = appendRandomId
|
||||
this.targetFilename = this.getSanitizedFilename(this.rssPodcastEpisode.title || '')
|
||||
this.targetFilename = this.getSanitizedFilename()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -126,9 +152,9 @@ class PodcastEpisodeDownload {
|
||||
this.url = encodeURI(url)
|
||||
}
|
||||
|
||||
this.targetFilename = this.getSanitizedFilename(this.rssPodcastEpisode.title || '')
|
||||
|
||||
this.libraryItem = libraryItem
|
||||
this.targetFilename = this.getSanitizedFilename()
|
||||
this.isAutoDownload = isAutoDownload
|
||||
this.createdAt = Date.now()
|
||||
this.libraryId = libraryId
|
||||
|
@ -175,6 +175,7 @@ function migratePodcast(oldLibraryItem, LibraryItem) {
|
||||
itunesArtistId: oldPodcastMetadata.itunesArtistId,
|
||||
language: oldPodcastMetadata.language,
|
||||
podcastType: oldPodcastMetadata.type,
|
||||
podcastFilenameFormat: oldPodcastMetadata.podcastFilenameFormat,
|
||||
explicit: !!oldPodcastMetadata.explicit,
|
||||
autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes,
|
||||
autoDownloadSchedule: oldPodcast.autoDownloadSchedule,
|
||||
|
Loading…
Reference in New Issue
Block a user