mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Fix:Book library collapse series with no-series filter #2976
This commit is contained in:
		
							parent
							
								
									ec07bfa940
								
							
						
					
					
						commit
						4cddc597c1
					
				| @ -35,7 +35,7 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|     // Validate that the custom provider exists if given any
 |     // Validate that the custom provider exists if given any
 | ||||||
|     if (newLibraryPayload.provider?.startsWith('custom-')) { |     if (newLibraryPayload.provider?.startsWith('custom-')) { | ||||||
|       if (!await Database.customMetadataProviderModel.checkExistsBySlug(newLibraryPayload.provider)) { |       if (!(await Database.customMetadataProviderModel.checkExistsBySlug(newLibraryPayload.provider))) { | ||||||
|         Logger.error(`[LibraryController] Custom metadata provider "${newLibraryPayload.provider}" does not exist`) |         Logger.error(`[LibraryController] Custom metadata provider "${newLibraryPayload.provider}" does not exist`) | ||||||
|         return res.status(400).send('Custom metadata provider does not exist') |         return res.status(400).send('Custom metadata provider does not exist') | ||||||
|       } |       } | ||||||
| @ -43,14 +43,15 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|     // Validate folder paths exist or can be created & resolve rel paths
 |     // Validate folder paths exist or can be created & resolve rel paths
 | ||||||
|     //   returns 400 if a folder fails to access
 |     //   returns 400 if a folder fails to access
 | ||||||
|     newLibraryPayload.folders = newLibraryPayload.folders.map(f => { |     newLibraryPayload.folders = newLibraryPayload.folders.map((f) => { | ||||||
|       f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath)) |       f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath)) | ||||||
|       return f |       return f | ||||||
|     }) |     }) | ||||||
|     for (const folder of newLibraryPayload.folders) { |     for (const folder of newLibraryPayload.folders) { | ||||||
|       try { |       try { | ||||||
|         const direxists = await fs.pathExists(folder.fullPath) |         const direxists = await fs.pathExists(folder.fullPath) | ||||||
|         if (!direxists) { // If folder does not exist try to make it and set file permissions/owner
 |         if (!direxists) { | ||||||
|  |           // If folder does not exist try to make it and set file permissions/owner
 | ||||||
|           await fs.mkdir(folder.fullPath) |           await fs.mkdir(folder.fullPath) | ||||||
|         } |         } | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
| @ -85,12 +86,12 @@ class LibraryController { | |||||||
|     const librariesAccessible = req.user.librariesAccessible || [] |     const librariesAccessible = req.user.librariesAccessible || [] | ||||||
|     if (librariesAccessible.length) { |     if (librariesAccessible.length) { | ||||||
|       return res.json({ |       return res.json({ | ||||||
|         libraries: libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON()) |         libraries: libraries.filter((lib) => librariesAccessible.includes(lib.id)).map((lib) => lib.toJSON()) | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
|       libraries: libraries.map(lib => lib.toJSON()) |       libraries: libraries.map((lib) => lib.toJSON()) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -140,7 +141,7 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|     // Validate that the custom provider exists if given any
 |     // Validate that the custom provider exists if given any
 | ||||||
|     if (req.body.provider?.startsWith('custom-')) { |     if (req.body.provider?.startsWith('custom-')) { | ||||||
|       if (!await Database.customMetadataProviderModel.checkExistsBySlug(req.body.provider)) { |       if (!(await Database.customMetadataProviderModel.checkExistsBySlug(req.body.provider))) { | ||||||
|         Logger.error(`[LibraryController] Custom metadata provider "${req.body.provider}" does not exist`) |         Logger.error(`[LibraryController] Custom metadata provider "${req.body.provider}" does not exist`) | ||||||
|         return res.status(400).send('Custom metadata provider does not exist') |         return res.status(400).send('Custom metadata provider does not exist') | ||||||
|       } |       } | ||||||
| @ -150,7 +151,7 @@ class LibraryController { | |||||||
|     //   returns 400 if a new folder fails to access
 |     //   returns 400 if a new folder fails to access
 | ||||||
|     if (req.body.folders) { |     if (req.body.folders) { | ||||||
|       const newFolderPaths = [] |       const newFolderPaths = [] | ||||||
|       req.body.folders = req.body.folders.map(f => { |       req.body.folders = req.body.folders.map((f) => { | ||||||
|         if (!f.id) { |         if (!f.id) { | ||||||
|           f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath)) |           f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath)) | ||||||
|           newFolderPaths.push(f.fullPath) |           newFolderPaths.push(f.fullPath) | ||||||
| @ -161,7 +162,10 @@ class LibraryController { | |||||||
|         const pathExists = await fs.pathExists(path) |         const pathExists = await fs.pathExists(path) | ||||||
|         if (!pathExists) { |         if (!pathExists) { | ||||||
|           // Ensure dir will recursively create directories which might be preferred over mkdir
 |           // Ensure dir will recursively create directories which might be preferred over mkdir
 | ||||||
|           const success = await fs.ensureDir(path).then(() => true).catch((error) => { |           const success = await fs | ||||||
|  |             .ensureDir(path) | ||||||
|  |             .then(() => true) | ||||||
|  |             .catch((error) => { | ||||||
|               Logger.error(`[LibraryController] Failed to ensure folder dir "${path}"`, error) |               Logger.error(`[LibraryController] Failed to ensure folder dir "${path}"`, error) | ||||||
|               return false |               return false | ||||||
|             }) |             }) | ||||||
| @ -173,7 +177,7 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|       // Handle removing folders
 |       // Handle removing folders
 | ||||||
|       for (const folder of library.folders) { |       for (const folder of library.folders) { | ||||||
|         if (!req.body.folders.some(f => f.id === folder.id)) { |         if (!req.body.folders.some((f) => f.id === folder.id)) { | ||||||
|           // Remove library items in folder
 |           // Remove library items in folder
 | ||||||
|           const libraryItemsInFolder = await Database.libraryItemModel.findAll({ |           const libraryItemsInFolder = await Database.libraryItemModel.findAll({ | ||||||
|             where: { |             where: { | ||||||
| @ -195,7 +199,7 @@ class LibraryController { | |||||||
|           for (const libraryItem of libraryItemsInFolder) { |           for (const libraryItem of libraryItemsInFolder) { | ||||||
|             let mediaItemIds = [] |             let mediaItemIds = [] | ||||||
|             if (library.isPodcast) { |             if (library.isPodcast) { | ||||||
|               mediaItemIds = libraryItem.media.podcastEpisodes.map(pe => pe.id) |               mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id) | ||||||
|             } else { |             } else { | ||||||
|               mediaItemIds.push(libraryItem.mediaId) |               mediaItemIds.push(libraryItem.mediaId) | ||||||
|             } |             } | ||||||
| @ -267,7 +271,7 @@ class LibraryController { | |||||||
|     for (const libraryItem of libraryItemsInLibrary) { |     for (const libraryItem of libraryItemsInLibrary) { | ||||||
|       let mediaItemIds = [] |       let mediaItemIds = [] | ||||||
|       if (library.isPodcast) { |       if (library.isPodcast) { | ||||||
|         mediaItemIds = libraryItem.media.podcastEpisodes.map(pe => pe.id) |         mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id) | ||||||
|       } else { |       } else { | ||||||
|         mediaItemIds.push(libraryItem.mediaId) |         mediaItemIds.push(libraryItem.mediaId) | ||||||
|       } |       } | ||||||
| @ -298,7 +302,10 @@ class LibraryController { | |||||||
|    * @param {import('express').Response} res |    * @param {import('express').Response} res | ||||||
|    */ |    */ | ||||||
|   async getLibraryItems(req, res) { |   async getLibraryItems(req, res) { | ||||||
|     const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) |     const include = (req.query.include || '') | ||||||
|  |       .split(',') | ||||||
|  |       .map((v) => v.trim().toLowerCase()) | ||||||
|  |       .filter((v) => !!v) | ||||||
| 
 | 
 | ||||||
|     const payload = { |     const payload = { | ||||||
|       results: [], |       results: [], | ||||||
| @ -316,7 +323,9 @@ class LibraryController { | |||||||
|     payload.offset = payload.page * payload.limit |     payload.offset = payload.page * payload.limit | ||||||
| 
 | 
 | ||||||
|     // TODO: Temporary way of handling collapse sub-series. Either remove feature or handle through sql queries
 |     // TODO: Temporary way of handling collapse sub-series. Either remove feature or handle through sql queries
 | ||||||
|     if (payload.filterBy?.split('.')[0] === 'series' && payload.collapseseries) { |     const filterByGroup = payload.filterBy?.split('.').shift() | ||||||
|  |     const filterByValue = filterByGroup ? libraryFilters.decode(payload.filterBy.replace(`${filterByGroup}.`, '')) : null | ||||||
|  |     if (filterByGroup === 'series' && filterByValue !== 'no-series' && payload.collapseseries) { | ||||||
|       const seriesId = libraryFilters.decode(payload.filterBy.split('.')[1]) |       const seriesId = libraryFilters.decode(payload.filterBy.split('.')[1]) | ||||||
|       payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.user, req.library) |       payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.user, req.library) | ||||||
|     } else { |     } else { | ||||||
| @ -369,7 +378,7 @@ class LibraryController { | |||||||
|     for (const libraryItem of libraryItemsWithIssues) { |     for (const libraryItem of libraryItemsWithIssues) { | ||||||
|       let mediaItemIds = [] |       let mediaItemIds = [] | ||||||
|       if (req.library.isPodcast) { |       if (req.library.isPodcast) { | ||||||
|         mediaItemIds = libraryItem.media.podcastEpisodes.map(pe => pe.id) |         mediaItemIds = libraryItem.media.podcastEpisodes.map((pe) => pe.id) | ||||||
|       } else { |       } else { | ||||||
|         mediaItemIds.push(libraryItem.mediaId) |         mediaItemIds.push(libraryItem.mediaId) | ||||||
|       } |       } | ||||||
| @ -393,7 +402,10 @@ class LibraryController { | |||||||
|    * @param {import('express').Response} res |    * @param {import('express').Response} res | ||||||
|    */ |    */ | ||||||
|   async getAllSeriesForLibrary(req, res) { |   async getAllSeriesForLibrary(req, res) { | ||||||
|     const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) |     const include = (req.query.include || '') | ||||||
|  |       .split(',') | ||||||
|  |       .map((v) => v.trim().toLowerCase()) | ||||||
|  |       .filter((v) => !!v) | ||||||
| 
 | 
 | ||||||
|     const payload = { |     const payload = { | ||||||
|       results: [], |       results: [], | ||||||
| @ -426,7 +438,10 @@ class LibraryController { | |||||||
|    * @param {import('express').Response} res - Series |    * @param {import('express').Response} res - Series | ||||||
|    */ |    */ | ||||||
|   async getSeriesForLibrary(req, res) { |   async getSeriesForLibrary(req, res) { | ||||||
|     const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) |     const include = (req.query.include || '') | ||||||
|  |       .split(',') | ||||||
|  |       .map((v) => v.trim().toLowerCase()) | ||||||
|  |       .filter((v) => !!v) | ||||||
| 
 | 
 | ||||||
|     const series = await Database.seriesModel.findByPk(req.params.seriesId) |     const series = await Database.seriesModel.findByPk(req.params.seriesId) | ||||||
|     if (!series) return res.sendStatus(404) |     if (!series) return res.sendStatus(404) | ||||||
| @ -436,10 +451,10 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|     const seriesJson = oldSeries.toJSON() |     const seriesJson = oldSeries.toJSON() | ||||||
|     if (include.includes('progress')) { |     if (include.includes('progress')) { | ||||||
|       const libraryItemsFinished = libraryItemsInSeries.filter(li => !!req.user.getMediaProgress(li.id)?.isFinished) |       const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.user.getMediaProgress(li.id)?.isFinished) | ||||||
|       seriesJson.progress = { |       seriesJson.progress = { | ||||||
|         libraryItemIds: libraryItemsInSeries.map(li => li.id), |         libraryItemIds: libraryItemsInSeries.map((li) => li.id), | ||||||
|         libraryItemIdsFinished: libraryItemsFinished.map(li => li.id), |         libraryItemIdsFinished: libraryItemsFinished.map((li) => li.id), | ||||||
|         isFinished: libraryItemsFinished.length >= libraryItemsInSeries.length |         isFinished: libraryItemsFinished.length >= libraryItemsInSeries.length | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @ -459,7 +474,10 @@ class LibraryController { | |||||||
|    * @param {*} res |    * @param {*} res | ||||||
|    */ |    */ | ||||||
|   async getCollectionsForLibrary(req, res) { |   async getCollectionsForLibrary(req, res) { | ||||||
|     const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) |     const include = (req.query.include || '') | ||||||
|  |       .split(',') | ||||||
|  |       .map((v) => v.trim().toLowerCase()) | ||||||
|  |       .filter((v) => !!v) | ||||||
| 
 | 
 | ||||||
|     const payload = { |     const payload = { | ||||||
|       results: [], |       results: [], | ||||||
| @ -495,7 +513,7 @@ class LibraryController { | |||||||
|    */ |    */ | ||||||
|   async getUserPlaylistsForLibrary(req, res) { |   async getUserPlaylistsForLibrary(req, res) { | ||||||
|     let playlistsForUser = await Database.playlistModel.getPlaylistsForUserAndLibrary(req.user.id, req.library.id) |     let playlistsForUser = await Database.playlistModel.getPlaylistsForUserAndLibrary(req.user.id, req.library.id) | ||||||
|     playlistsForUser = await Promise.all(playlistsForUser.map(async p => p.getOldJsonExpanded())) |     playlistsForUser = await Promise.all(playlistsForUser.map(async (p) => p.getOldJsonExpanded())) | ||||||
| 
 | 
 | ||||||
|     const payload = { |     const payload = { | ||||||
|       results: [], |       results: [], | ||||||
| @ -531,7 +549,10 @@ class LibraryController { | |||||||
|    */ |    */ | ||||||
|   async getUserPersonalizedShelves(req, res) { |   async getUserPersonalizedShelves(req, res) { | ||||||
|     const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10 |     const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10 | ||||||
|     const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) |     const include = (req.query.include || '') | ||||||
|  |       .split(',') | ||||||
|  |       .map((v) => v.trim().toLowerCase()) | ||||||
|  |       .filter((v) => !!v) | ||||||
|     const shelves = await Database.libraryItemModel.getPersonalizedShelves(req.library, req.user, include, limitPerShelf) |     const shelves = await Database.libraryItemModel.getPersonalizedShelves(req.library, req.user, include, limitPerShelf) | ||||||
|     res.json(shelves) |     res.json(shelves) | ||||||
|   } |   } | ||||||
| @ -552,7 +573,7 @@ class LibraryController { | |||||||
|     const orderdata = req.body |     const orderdata = req.body | ||||||
|     let hasUpdates = false |     let hasUpdates = false | ||||||
|     for (let i = 0; i < orderdata.length; i++) { |     for (let i = 0; i < orderdata.length; i++) { | ||||||
|       const library = libraries.find(lib => lib.id === orderdata[i].id) |       const library = libraries.find((lib) => lib.id === orderdata[i].id) | ||||||
|       if (!library) { |       if (!library) { | ||||||
|         Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`) |         Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`) | ||||||
|         return res.sendStatus(500) |         return res.sendStatus(500) | ||||||
| @ -571,7 +592,7 @@ class LibraryController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
|       libraries: libraries.map(lib => lib.toJSON()) |       libraries: libraries.map((lib) => lib.toJSON()) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -657,9 +678,7 @@ class LibraryController { | |||||||
|           attributes: [] |           attributes: [] | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       order: [ |       order: [[Sequelize.literal('name COLLATE NOCASE'), 'ASC']] | ||||||
|         [Sequelize.literal('name COLLATE NOCASE'), 'ASC'] |  | ||||||
|       ] |  | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     const oldAuthors = [] |     const oldAuthors = [] | ||||||
| @ -699,7 +718,7 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|     const narrators = {} |     const narrators = {} | ||||||
|     for (const book of booksWithNarrators) { |     for (const book of booksWithNarrators) { | ||||||
|       book.narrators.forEach(n => { |       book.narrators.forEach((n) => { | ||||||
|         if (typeof n !== 'string') { |         if (typeof n !== 'string') { | ||||||
|           Logger.error(`[LibraryController] getNarrators: Invalid narrator "${n}" on book "${book.title}"`) |           Logger.error(`[LibraryController] getNarrators: Invalid narrator "${n}" on book "${book.title}"`) | ||||||
|         } else if (!narrators[n]) { |         } else if (!narrators[n]) { | ||||||
| @ -715,7 +734,7 @@ class LibraryController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
|       narrators: naturalSort(Object.values(narrators)).asc(n => n.name) |       narrators: naturalSort(Object.values(narrators)).asc((n) => n.name) | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -747,7 +766,7 @@ class LibraryController { | |||||||
|     const itemsWithNarrator = await libraryItemFilters.getAllLibraryItemsWithNarrators([narratorName]) |     const itemsWithNarrator = await libraryItemFilters.getAllLibraryItemsWithNarrators([narratorName]) | ||||||
| 
 | 
 | ||||||
|     for (const libraryItem of itemsWithNarrator) { |     for (const libraryItem of itemsWithNarrator) { | ||||||
|       libraryItem.media.narrators = libraryItem.media.narrators.filter(n => n !== narratorName) |       libraryItem.media.narrators = libraryItem.media.narrators.filter((n) => n !== narratorName) | ||||||
|       if (!libraryItem.media.narrators.includes(updatedName)) { |       if (!libraryItem.media.narrators.includes(updatedName)) { | ||||||
|         libraryItem.media.narrators.push(updatedName) |         libraryItem.media.narrators.push(updatedName) | ||||||
|       } |       } | ||||||
| @ -759,7 +778,10 @@ class LibraryController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (itemsUpdated.length) { |     if (itemsUpdated.length) { | ||||||
|       SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded())) |       SocketAuthority.emitter( | ||||||
|  |         'items_updated', | ||||||
|  |         itemsUpdated.map((li) => li.toJSONExpanded()) | ||||||
|  |       ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
| @ -790,7 +812,7 @@ class LibraryController { | |||||||
|     const itemsWithNarrator = await libraryItemFilters.getAllLibraryItemsWithNarrators([narratorName]) |     const itemsWithNarrator = await libraryItemFilters.getAllLibraryItemsWithNarrators([narratorName]) | ||||||
| 
 | 
 | ||||||
|     for (const libraryItem of itemsWithNarrator) { |     for (const libraryItem of itemsWithNarrator) { | ||||||
|       libraryItem.media.narrators = libraryItem.media.narrators.filter(n => n !== narratorName) |       libraryItem.media.narrators = libraryItem.media.narrators.filter((n) => n !== narratorName) | ||||||
|       await libraryItem.media.update({ |       await libraryItem.media.update({ | ||||||
|         narrators: libraryItem.media.narrators |         narrators: libraryItem.media.narrators | ||||||
|       }) |       }) | ||||||
| @ -799,7 +821,10 @@ class LibraryController { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (itemsUpdated.length) { |     if (itemsUpdated.length) { | ||||||
|       SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded())) |       SocketAuthority.emitter( | ||||||
|  |         'items_updated', | ||||||
|  |         itemsUpdated.map((li) => li.toJSONExpanded()) | ||||||
|  |       ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     res.json({ |     res.json({ | ||||||
| @ -859,7 +884,7 @@ class LibraryController { | |||||||
|     const payload = { |     const payload = { | ||||||
|       episodes: [], |       episodes: [], | ||||||
|       limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, |       limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0, | ||||||
|       page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0, |       page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const offset = payload.page * payload.limit |     const offset = payload.page * payload.limit | ||||||
| @ -929,10 +954,10 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|     let numRemoved = 0 |     let numRemoved = 0 | ||||||
|     for (const libraryItem of libraryItemsWithMetadata) { |     for (const libraryItem of libraryItemsWithMetadata) { | ||||||
|       const metadataFilepath = libraryItem.libraryFiles.find(lf => lf.metadata.filename === metadataFilename)?.metadata.path |       const metadataFilepath = libraryItem.libraryFiles.find((lf) => lf.metadata.filename === metadataFilename)?.metadata.path | ||||||
|       if (!metadataFilepath) continue |       if (!metadataFilepath) continue | ||||||
|       Logger.debug(`[LibraryController] Removing file "${metadataFilepath}"`) |       Logger.debug(`[LibraryController] Removing file "${metadataFilepath}"`) | ||||||
|       if ((await fileUtils.removeFile(metadataFilepath))) { |       if (await fileUtils.removeFile(metadataFilepath)) { | ||||||
|         numRemoved++ |         numRemoved++ | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user