diff --git a/.eslintrc b/.eslintrc index c234081..cf46c54 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,7 @@ "root": true, "parser": "@typescript-eslint/parser", "env": { "node": true }, - "plugins": ["@typescript-eslint"], + "plugins": ["@typescript-eslint", "only-warn"], "extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"], "parserOptions": { "sourceType": "module" @@ -12,6 +12,9 @@ "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], "@typescript-eslint/ban-ts-comment": "off", "no-prototype-builtins": "off", - "@typescript-eslint/no-empty-function": "off" + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-function-return-type": ["warn"] } } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89b7d92..bc02e9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,11 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: - node-version: '14.x' # You might need to adjust this value to your own version + node-version: '16.x' # You might need to adjust this value to your own version - name: Build id: build run: | @@ -28,7 +28,8 @@ jobs: cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} ls - echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" + echo "tag_name=$(git tag --sort version:refname | tail -n 1)" >> $GITHUB_OUTPUT + - name: Create Release id: create_release uses: actions/create-release@v1 @@ -40,6 +41,7 @@ jobs: release_name: ${{ github.ref }} draft: false prerelease: false + - name: Upload zip file id: upload-zip uses: actions/upload-release-asset@v1 @@ -50,6 +52,7 @@ jobs: asset_path: ./${{ env.PLUGIN_NAME }}.zip asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip asset_content_type: application/zip + - name: Upload main.js id: upload-main uses: actions/upload-release-asset@v1 @@ -60,6 +63,7 @@ jobs: asset_path: ./main.js asset_name: main.js asset_content_type: text/javascript + - name: Upload manifest.json id: upload-manifest uses: actions/upload-release-asset@v1 @@ -70,6 +74,7 @@ jobs: asset_path: ./manifest.json asset_name: manifest.json asset_content_type: application/json + - name: Upload styles.css id: upload-css uses: actions/upload-release-asset@v1 diff --git a/LICENSE.md b/LICENSE.md index a5eae15..61c085e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,11 +1,12 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 +# GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble +## Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. @@ -68,9 +69,9 @@ patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. - TERMS AND CONDITIONS +## TERMS AND CONDITIONS -0. Definitions. +### 0. Definitions. "This License" refers to version 3 of the GNU General Public License. @@ -109,7 +110,7 @@ work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. -1. Source Code. +### 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source @@ -151,7 +152,7 @@ Source. The Corresponding Source for a work in source code form is that same work. -2. Basic Permissions. +### 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated @@ -176,7 +177,7 @@ Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. -3. Protecting Users' Legal Rights From Anti-Circumvention Law. +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article @@ -192,7 +193,7 @@ modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. -4. Conveying Verbatim Copies. +### 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and @@ -205,32 +206,32 @@ recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. -5. Conveying Modified Source Versions. +### 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. +a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". +b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +"keep intact all notices". - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. +c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, @@ -242,53 +243,53 @@ beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. -6. Conveying Non-Source Forms. +### 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either (1) a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. +d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. +e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be @@ -340,7 +341,7 @@ documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. -7. Additional Terms. +### 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. @@ -362,28 +363,28 @@ Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or +e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. +f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you @@ -404,7 +405,7 @@ Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. -8. Termination. +### 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or @@ -432,7 +433,7 @@ this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. -9. Acceptance Not Required for Having Copies. +### 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work @@ -443,7 +444,7 @@ modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. -10. Automatic Licensing of Downstream Recipients. +### 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and @@ -468,7 +469,7 @@ rights granted under this License, and you may not initiate litigation any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. -11. Patents. +### 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The @@ -537,7 +538,7 @@ Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. -12. No Surrender of Others' Freedom. +### 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not @@ -549,7 +550,7 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. -13. Use with the GNU Affero General Public License. +### 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed @@ -560,7 +561,7 @@ but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. -14. Revised Versions of this License. +### 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will @@ -586,7 +587,7 @@ permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. -15. Disclaimer of Warranty. +### 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT @@ -597,7 +598,7 @@ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. -16. Limitation of Liability. +### 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS @@ -609,7 +610,7 @@ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -17. Interpretation of Sections 15 and 16. +### 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, @@ -618,57 +619,4 @@ an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - -If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - -You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - -The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +## END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md index 2253d70..bf1042e 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,15 @@ You are more than welcome to open an issue on [GitHub](https://github.com/mProje ### Changelog +#### 0.5.0 + +- New simple search modal, select the media type and search all applicable APIs +- More data for Board Games +- Actors and Streaming Platforms for Movies and Series +- Separate new file location for all media types +- Separate command for each media type +- Fix problems with closing of preview modal + #### 0.3.2 - Added Board Game Geek API (documentation pending) diff --git a/manifest.json b/manifest.json index 9e5e796..ed1a452 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-media-db-plugin", "name": "Media DB Plugin", - "version": "0.4.1", + "version": "0.5.1", "minAppVersion": "0.14.0", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "author": "Moritz Jung", diff --git a/package-lock.json b/package-lock.json index 57e7d14..dd135d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "builtin-modules": "^3.2.0", "esbuild": "0.13.12", "esbuild-svelte": "^0.7.1", + "eslint-plugin-only-warn": "^1.0.3", "jest": "^28.1.2", "jest-fetch-mock": "^3.0.3", "obsidian": "latest", @@ -2664,6 +2665,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-only-warn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.3.tgz", + "integrity": "sha512-XQOX/TfLoLw6h8ky51d29uUjXRTQHqBGXPylDEmy5fe/w7LIOnp8MA24b1OSMEn9BQoKow1q3g1kLe5/9uBTvw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -7649,6 +7659,12 @@ } } }, + "eslint-plugin-only-warn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.3.tgz", + "integrity": "sha512-XQOX/TfLoLw6h8ky51d29uUjXRTQHqBGXPylDEmy5fe/w7LIOnp8MA24b1OSMEn9BQoKow1q3g1kLe5/9uBTvw==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/package.json b/package.json index 3fb465e..656b348 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-media-db-plugin", - "version": "0.4.1", + "version": "0.5.1", "description": "A plugin that can query multiple APIs for movies, series, anime, games, music and wiki articles, and import them into your vault.", "main": "main.js", "scripts": { @@ -23,6 +23,7 @@ "builtin-modules": "^3.2.0", "esbuild": "0.13.12", "esbuild-svelte": "^0.7.1", + "eslint-plugin-only-warn": "^1.0.3", "jest": "^28.1.2", "jest-fetch-mock": "^3.0.3", "obsidian": "latest", diff --git a/src/api/APIModel.ts b/src/api/APIModel.ts index c44589c..c424508 100644 --- a/src/api/APIModel.ts +++ b/src/api/APIModel.ts @@ -1,10 +1,11 @@ import { MediaTypeModel } from '../models/MediaTypeModel'; +import { MediaType } from '../utils/MediaType'; export abstract class APIModel { apiName: string; apiUrl: string; apiDescription: string; - types: string[]; + types: MediaType[]; /** * This function should query the api and return a list of matches. The matches should be caped at 20. @@ -15,12 +16,11 @@ export abstract class APIModel { abstract getById(id: string): Promise; - hasType(type: string): boolean { + hasType(type: MediaType): boolean { return this.types.contains(type); } - // for future use (https://github.com/mProjectsCode/obsidian-media-db-plugin/issues/5) - hasTypeOverlap(types: string[]): boolean { + hasTypeOverlap(types: MediaType[]): boolean { for (const type of types) { if (this.hasType(type)) { return true; diff --git a/src/api/apis/BoardGameGeekAPI.ts b/src/api/apis/BoardGameGeekAPI.ts index f49955f..fb8a470 100644 --- a/src/api/apis/BoardGameGeekAPI.ts +++ b/src/api/apis/BoardGameGeekAPI.ts @@ -2,8 +2,8 @@ import { APIModel } from '../APIModel'; import { MediaTypeModel } from '../../models/MediaTypeModel'; import MediaDbPlugin from '../../main'; import { BoardGameModel } from 'src/models/BoardGameModel'; -import { debugLog } from '../../utils/Utils'; import { requestUrl } from 'obsidian'; +import { MediaType } from '../../utils/MediaType'; export class BoardGameGeekAPI extends APIModel { plugin: MediaDbPlugin; @@ -15,7 +15,7 @@ export class BoardGameGeekAPI extends APIModel { this.apiName = 'BoardGameGeekAPI'; this.apiDescription = 'A free API for BoardGameGeek things.'; this.apiUrl = 'https://api.geekdo.com/xmlapi'; - this.types = ['boardgames']; + this.types = [MediaType.BoardGame]; } async searchByTitle(title: string): Promise { @@ -33,13 +33,13 @@ export class BoardGameGeekAPI extends APIModel { const data = fetchData.text; const response = new window.DOMParser().parseFromString(data, 'text/xml'); - debugLog(response); + console.debug(response); - let ret: MediaTypeModel[] = []; + const ret: MediaTypeModel[] = []; for (const boardgame of Array.from(response.querySelectorAll('boardgame'))) { const id = boardgame.attributes.getNamedItem('objectid')!.value; - const title = boardgame.querySelector('name')!.textContent!; + const title = boardgame.querySelector('name[primary=true]')?.textContent ?? boardgame.querySelector('name')!.textContent!; const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; ret.push( @@ -70,17 +70,22 @@ export class BoardGameGeekAPI extends APIModel { const data = fetchData.text; const response = new window.DOMParser().parseFromString(data, 'text/xml'); - debugLog(response); + console.debug(response); const boardgame = response.querySelector('boardgame')!; - const title = boardgame.querySelector('name')!.textContent!; + const title = boardgame.querySelector('name[primary=true]')!.textContent!; const year = boardgame.querySelector('yearpublished')?.textContent ?? ''; const image = boardgame.querySelector('image')?.textContent ?? undefined; - const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? ''); + const onlineRating = Number.parseFloat(boardgame.querySelector('statistics ratings average')?.textContent ?? '0'); const genres = Array.from(boardgame.querySelectorAll('boardgamecategory')).map(n => n!.textContent!); + const complexityRating = Number.parseFloat(boardgame.querySelector('averageweight')?.textContent ?? '0'); + const minPlayers = Number.parseFloat(boardgame.querySelector('minplayers')?.textContent ?? '0'); + const maxPlayers = Number.parseFloat(boardgame.querySelector('maxplayers')?.textContent ?? '0'); + const playtime = (boardgame.querySelector('playingtime')?.textContent ?? 'unknown') + ' minutes'; + const publishers = Array.from(boardgame.querySelectorAll('boardgamepublisher')).map(n => n!.textContent!); const model = new BoardGameModel({ - title, + title: title, englishTitle: title, year: year === '0' ? '' : year, dataSource: this.apiName, @@ -89,7 +94,13 @@ export class BoardGameGeekAPI extends APIModel { genres: genres, onlineRating: onlineRating, + complexityRating: complexityRating, + minPlayers: minPlayers, + maxPlayers: maxPlayers, + playtime: playtime, + publishers: publishers, image: image, + released: true, userData: { diff --git a/src/api/apis/LocGovAPI.ts b/src/api/apis/LocGovAPI.ts index b7f843a..97addde 100644 --- a/src/api/apis/LocGovAPI.ts +++ b/src/api/apis/LocGovAPI.ts @@ -1,7 +1,6 @@ import { APIModel } from '../APIModel'; import { MediaTypeModel } from '../../models/MediaTypeModel'; import MediaDbPlugin from '../../main'; -import { debugLog } from '../../utils/Utils'; // WIP export class LocGovAPI extends APIModel { @@ -25,14 +24,14 @@ export class LocGovAPI extends APIModel { const searchUrl = `https://www.loc.gov/search/?q=${encodeURIComponent(title)}&fo=json&c=20`; const fetchData = await fetch(searchUrl); - debugLog(fetchData); + console.debug(fetchData); if (fetchData.status !== 200) { throw Error(`MDB | Received status code ${fetchData.status} from an API.`); } const data = await fetchData.json(); - debugLog(data); + console.debug(data); let ret: MediaTypeModel[] = []; throw new Error('MDB | Under construction, API implementation not finished'); @@ -50,7 +49,7 @@ export class LocGovAPI extends APIModel { } const data = await fetchData.json(); - debugLog(data); + console.debug(data); const result = data.data; const type = this.typeMappings.get(result.type.toLowerCase()); diff --git a/src/api/apis/MALAPI.ts b/src/api/apis/MALAPI.ts index 880e008..a728316 100644 --- a/src/api/apis/MALAPI.ts +++ b/src/api/apis/MALAPI.ts @@ -3,7 +3,7 @@ import { MediaTypeModel } from '../../models/MediaTypeModel'; import { MovieModel } from '../../models/MovieModel'; import MediaDbPlugin from '../../main'; import { SeriesModel } from '../../models/SeriesModel'; -import { debugLog } from '../../utils/Utils'; +import { MediaType } from '../../utils/MediaType'; export class MALAPI extends APIModel { plugin: MediaDbPlugin; @@ -16,7 +16,7 @@ export class MALAPI extends APIModel { this.apiName = 'MALAPI'; this.apiDescription = 'A free API for Anime. Some results may take a long time to load.'; this.apiUrl = 'https://jikan.moe/'; - this.types = ['movie', 'series', 'anime']; + this.types = [MediaType.Movie, MediaType.Series]; this.typeMappings = new Map(); this.typeMappings.set('movie', 'movie'); this.typeMappings.set('special', 'special'); @@ -30,15 +30,15 @@ export class MALAPI extends APIModel { const searchUrl = `https://api.jikan.moe/v4/anime?q=${encodeURIComponent(title)}&limit=20${this.plugin.settings.sfwFilter ? '&sfw' : ''}`; const fetchData = await fetch(searchUrl); - debugLog(fetchData); + console.debug(fetchData); if (fetchData.status !== 200) { throw Error(`MDB | Received status code ${fetchData.status} from an API.`); } const data = await fetchData.json(); - debugLog(data); + console.debug(data); - let ret: MediaTypeModel[] = []; + const ret: MediaTypeModel[] = []; for (const result of data.data) { const type = this.typeMappings.get(result.type?.toLowerCase()); @@ -85,7 +85,7 @@ export class MALAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const searchUrl = `https://api.jikan.moe/v4/anime/${encodeURIComponent(id)}`; + const searchUrl = `https://api.jikan.moe/v4/anime/${encodeURIComponent(id)}/full`; const fetchData = await fetch(searchUrl); if (fetchData.status !== 200) { @@ -93,7 +93,7 @@ export class MALAPI extends APIModel { } const data = await fetchData.json(); - debugLog(data); + console.debug(data); const result = data.data; const type = this.typeMappings.get(result.type?.toLowerCase()); @@ -111,10 +111,12 @@ export class MALAPI extends APIModel { producer: result.studios?.map((x: any) => x.name).join(', ') ?? 'unknown', duration: result.duration ?? 'unknown', onlineRating: result.score ?? 0, + actors: [], image: result.images?.jpg?.image_url ?? '', released: true, premiere: new Date(result.aired?.from).toLocaleDateString() ?? 'unknown', + streamingServices: result.streaming?.map((x: any) => x.name) ?? [], userData: { watched: false, @@ -140,10 +142,12 @@ export class MALAPI extends APIModel { producer: result.studios?.map((x: any) => x.name).join(', ') ?? 'unknown', duration: result.duration ?? 'unknown', onlineRating: result.score ?? 0, + actors: [], image: result.images?.jpg?.image_url ?? '', released: true, premiere: new Date(result.aired?.from).toLocaleDateString() ?? 'unknown', + streamingServices: result.streaming?.map((x: any) => x.name) ?? [], userData: { watched: false, @@ -168,6 +172,7 @@ export class MALAPI extends APIModel { episodes: result.episodes, duration: result.duration ?? 'unknown', onlineRating: result.score ?? 0, + streamingServices: result.streaming?.map((x: any) => x.name) ?? [], image: result.images?.jpg?.image_url ?? '', released: true, diff --git a/src/api/apis/MusicBrainzAPI.ts b/src/api/apis/MusicBrainzAPI.ts index 0b85b17..a731ecc 100644 --- a/src/api/apis/MusicBrainzAPI.ts +++ b/src/api/apis/MusicBrainzAPI.ts @@ -3,7 +3,8 @@ import { MediaTypeModel } from '../../models/MediaTypeModel'; import MediaDbPlugin from '../../main'; import { requestUrl } from 'obsidian'; import { MusicReleaseModel } from '../../models/MusicReleaseModel'; -import { contactEmail, debugLog, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { contactEmail, mediaDbVersion, pluginName } from '../../utils/Utils'; +import { MediaType } from '../../utils/MediaType'; export class MusicBrainzAPI extends APIModel { plugin: MediaDbPlugin; @@ -15,7 +16,7 @@ export class MusicBrainzAPI extends APIModel { this.apiName = 'MusicBrainz API'; this.apiDescription = 'Free API for music albums.'; this.apiUrl = 'https://musicbrainz.org/'; - this.types = ['music']; + this.types = [MediaType.MusicRelease]; } async searchByTitle(title: string): Promise { @@ -30,15 +31,15 @@ export class MusicBrainzAPI extends APIModel { }, }); - debugLog(fetchData); + console.debug(fetchData); if (fetchData.status !== 200) { throw Error(`MDB | Received status code ${fetchData.status} from an API.`); } const data = await fetchData.json; - debugLog(data); - let ret: MediaTypeModel[] = []; + console.debug(data); + const ret: MediaTypeModel[] = []; for (const result of data['release-groups']) { ret.push( @@ -76,7 +77,7 @@ export class MusicBrainzAPI extends APIModel { } const data = await fetchData.json; - debugLog(data); + console.debug(data); const result = data; const model = new MusicReleaseModel({ diff --git a/src/api/apis/OMDbAPI.ts b/src/api/apis/OMDbAPI.ts index 0a7af61..3b7ca30 100644 --- a/src/api/apis/OMDbAPI.ts +++ b/src/api/apis/OMDbAPI.ts @@ -4,7 +4,7 @@ import { MovieModel } from '../../models/MovieModel'; import MediaDbPlugin from '../../main'; import { SeriesModel } from '../../models/SeriesModel'; import { GameModel } from '../../models/GameModel'; -import { debugLog } from '../../utils/Utils'; +import { MediaType } from '../../utils/MediaType'; export class OMDbAPI extends APIModel { plugin: MediaDbPlugin; @@ -17,7 +17,7 @@ export class OMDbAPI extends APIModel { this.apiName = 'OMDbAPI'; this.apiDescription = 'A free API for Movies, Series and Games.'; this.apiUrl = 'http://www.omdbapi.com/'; - this.types = ['movie', 'series']; + this.types = [MediaType.Movie, MediaType.Series, MediaType.Game]; this.typeMappings = new Map(); this.typeMappings.set('movie', 'movie'); this.typeMappings.set('series', 'series'); @@ -40,15 +40,19 @@ export class OMDbAPI extends APIModel { const data = await fetchData.json(); if (data.Response === 'False') { - throw Error(`MDB | Received error from ${this.apiName}: ${data.Error}`); + if (data.Error === 'Movie not found!') { + return []; + } + + throw Error(`MDB | Received error from ${this.apiName}: \n${JSON.stringify(data, undefined, 4)}`); } if (!data.Search) { return []; } - debugLog(data.Search); + console.debug(data.Search); - let ret: MediaTypeModel[] = []; + const ret: MediaTypeModel[] = []; for (const result of data.Search) { const type = this.typeMappings.get(result.Type.toLowerCase()); @@ -108,7 +112,7 @@ export class OMDbAPI extends APIModel { } const result = await fetchData.json(); - debugLog(result); + console.debug(result); if (result.Response === 'False') { throw Error(`MDB | Received error from ${this.apiName}: ${result.Error}`); @@ -133,9 +137,11 @@ export class OMDbAPI extends APIModel { producer: result.Director ?? 'unknown', duration: result.Runtime ?? 'unknown', onlineRating: Number.parseFloat(result.imdbRating ?? 0), + actors: result.Actors?.split(', ') ?? [], image: result.Poster ?? '', released: true, + streamingServices: [], premiere: new Date(result.Released).toLocaleDateString() ?? 'unknown', userData: { @@ -161,9 +167,11 @@ export class OMDbAPI extends APIModel { episodes: 0, duration: result.Runtime ?? 'unknown', onlineRating: Number.parseFloat(result.imdbRating ?? 0), + actors: result.Actors?.split(', ') ?? [], image: result.Poster ?? '', released: true, + streamingServices: [], airing: false, airedFrom: new Date(result.Released).toLocaleDateString() ?? 'unknown', airedTo: 'unknown', diff --git a/src/api/apis/SteamAPI.ts b/src/api/apis/SteamAPI.ts index 4cea1be..ae36d3a 100644 --- a/src/api/apis/SteamAPI.ts +++ b/src/api/apis/SteamAPI.ts @@ -2,7 +2,6 @@ import { APIModel } from '../APIModel'; import { MediaTypeModel } from '../../models/MediaTypeModel'; import MediaDbPlugin from '../../main'; import { GameModel } from '../../models/GameModel'; -import { debugLog } from '../../utils/Utils'; import { requestUrl } from 'obsidian'; import { MediaType } from '../../utils/MediaType'; @@ -17,7 +16,7 @@ export class SteamAPI extends APIModel { this.apiName = 'SteamAPI'; this.apiDescription = 'A free API for all Steam games.'; this.apiUrl = 'http://www.steampowered.com/'; - this.types = ['games']; + this.types = [MediaType.Game]; this.typeMappings = new Map(); this.typeMappings.set('game', 'game'); } @@ -36,9 +35,9 @@ export class SteamAPI extends APIModel { const data = await fetchData.json; - debugLog(data); + console.debug(data); - let filteredData = []; + const filteredData = []; for (const app of data.applist.apps) { if (app.name.toLowerCase().includes(title.toLowerCase())) { @@ -49,7 +48,7 @@ export class SteamAPI extends APIModel { } } - let ret: MediaTypeModel[] = []; + const ret: MediaTypeModel[] = []; for (const result of filteredData) { ret.push( @@ -70,7 +69,7 @@ export class SteamAPI extends APIModel { async getById(id: string): Promise { console.log(`MDB | api "${this.apiName}" queried by ID`); - const searchUrl = `http://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}`; + const searchUrl = `http://store.steampowered.com/api/appdetails?appids=${encodeURIComponent(id)}&l=en`; const fetchData = await requestUrl({ url: searchUrl, }); @@ -79,7 +78,7 @@ export class SteamAPI extends APIModel { throw Error(`MDB | Received status code ${fetchData.status} from an API.`); } - debugLog(await fetchData.json); + console.debug(await fetchData.json); let result: any; for (const [key, value] of Object.entries(await fetchData.json)) { @@ -94,7 +93,7 @@ export class SteamAPI extends APIModel { throw Error(`MDB | API returned invalid data.`); } - debugLog(result); + console.debug(result); const model = new GameModel({ type: MediaType.Game, diff --git a/src/api/apis/WikipediaAPI.ts b/src/api/apis/WikipediaAPI.ts index d51dd69..5193bb4 100644 --- a/src/api/apis/WikipediaAPI.ts +++ b/src/api/apis/WikipediaAPI.ts @@ -2,7 +2,7 @@ import { APIModel } from '../APIModel'; import { MediaTypeModel } from '../../models/MediaTypeModel'; import MediaDbPlugin from '../../main'; import { WikiModel } from '../../models/WikiModel'; -import { debugLog } from '../../utils/Utils'; +import { MediaType } from '../../utils/MediaType'; export class WikipediaAPI extends APIModel { plugin: MediaDbPlugin; @@ -14,7 +14,7 @@ export class WikipediaAPI extends APIModel { this.apiName = 'Wikipedia API'; this.apiDescription = 'The API behind Wikipedia'; this.apiUrl = 'https://www.wikipedia.com'; - this.types = ['wiki']; + this.types = [MediaType.Wiki]; } async searchByTitle(title: string): Promise { @@ -22,15 +22,15 @@ export class WikipediaAPI extends APIModel { const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}&srlimit=20&utf8=&format=json&origin=*`; const fetchData = await fetch(searchUrl); - debugLog(fetchData); + console.debug(fetchData); if (fetchData.status !== 200) { throw Error(`MDB | Received status code ${fetchData.status} from an API.`); } const data = await fetchData.json(); - debugLog(data); - let ret: MediaTypeModel[] = []; + console.debug(data); + const ret: MediaTypeModel[] = []; for (const result of data.query.search) { ret.push( @@ -59,7 +59,7 @@ export class WikipediaAPI extends APIModel { } const data = await fetchData.json(); - debugLog(data); + console.debug(data); const result: any = Object.entries(data?.query?.pages)[0][1]; const model = new WikiModel({ diff --git a/src/main.ts b/src/main.ts index acff744..76abff4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,7 +25,7 @@ export default class MediaDbPlugin extends Plugin { frontMatterRexExpPattern: string = '^(---)\\n[\\s\\S]*?\\n---'; - async onload() { + async onload(): Promise { this.apiManager = new APIManager(); // register APIs this.apiManager.registerAPI(new OMDbAPI(this)); @@ -45,9 +45,10 @@ export default class MediaDbPlugin extends Plugin { this.addSettingTab(new MediaDbSettingTab(this.app, this)); this.mediaTypeManager.updateTemplates(this.settings); + this.mediaTypeManager.updateFolders(this.settings); // add icon to the left ribbon - const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', (evt: MouseEvent) => this.createEntryWithAdvancedSearchModal()); + const ribbonIconEl = this.addRibbonIcon('database', 'Add new Media DB entry', () => this.createEntryWithAdvancedSearchModal()); ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class'); this.registerEvent( @@ -135,8 +136,8 @@ export default class MediaDbPlugin extends Plugin { * - replace the detail query * - maybe custom link syntax */ - async createLinkWithSearchModal() { - let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { + async createLinkWithSearchModal(): Promise { + const apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); }); @@ -162,7 +163,7 @@ export default class MediaDbPlugin extends Plugin { } } - async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions) { + async createEntryWithSearchModal(searchModalOptions?: SearchModalOptions): Promise { let types: string[] = []; let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openSearchModal(searchModalOptions ?? {}, async searchModalData => { types = searchModalData.types; @@ -197,8 +198,8 @@ export default class MediaDbPlugin extends Plugin { await this.createMediaDbNotes(selectResults); } - async createEntryWithAdvancedSearchModal() { - let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { + async createEntryWithAdvancedSearchModal(): Promise { + const apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async advancedSearchModalData => { return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis); }); @@ -253,7 +254,7 @@ export default class MediaDbPlugin extends Plugin { } async queryDetails(models: MediaTypeModel[]): Promise { - let detailModels: MediaTypeModel[] = []; + const detailModels: MediaTypeModel[] = []; for (const model of models) { try { detailModels.push(await this.apiManager.queryDetailedInfo(model)); @@ -269,7 +270,11 @@ export default class MediaDbPlugin extends Plugin { try { console.debug('MDB | creating new note'); - let fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, options); + const fileContent = await this.generateMediaDbNoteContents(mediaTypeModel, options); + + if (!options.folder) { + options.folder = await this.mediaTypeManager.getFolder(mediaTypeModel, this.app); + } await this.createNote(this.mediaTypeManager.getFileName(mediaTypeModel), fileContent, options); } catch (e) { @@ -278,7 +283,7 @@ export default class MediaDbPlugin extends Plugin { } } - async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions) { + async generateMediaDbNoteContents(mediaTypeModel: MediaTypeModel, options: CreateNoteOptions): Promise { let fileMetadata = this.modelPropertyMapper.convertObject(mediaTypeModel.toMetaDataObject()); let fileContent = ''; const template = options.attachTemplate ? await this.mediaTypeManager.getTemplate(mediaTypeModel, this.app) : ''; @@ -295,7 +300,7 @@ export default class MediaDbPlugin extends Plugin { return { fileMetadata: fileMetadata, fileContent: fileContent }; } - let attachFileMetadata: any = this.getMetadataFromFileCache(fileToAttach); + const attachFileMetadata: any = this.getMetadataFromFileCache(fileToAttach); fileMetadata = Object.assign(attachFileMetadata, fileMetadata); let attachFileContent: string = await this.app.vault.read(fileToAttach); @@ -312,7 +317,7 @@ export default class MediaDbPlugin extends Plugin { return { fileMetadata: fileMetadata, fileContent: fileContent }; } - let templateMetadata: any = this.getMetaDataFromFileContent(template); + const templateMetadata: any = this.getMetaDataFromFileContent(template); fileMetadata = Object.assign(templateMetadata, fileMetadata); const regExp = new RegExp(this.frontMatterRexExpPattern); @@ -348,7 +353,7 @@ export default class MediaDbPlugin extends Plugin { return metadata; } - getMetadataFromFileCache(file: TFile) { + getMetadataFromFileCache(file: TFile): any { let metadata: any = this.app.metadataCache.getFileCache(file).frontmatter; if (metadata) { metadata = Object.assign({}, metadata); // copy @@ -366,12 +371,9 @@ export default class MediaDbPlugin extends Plugin { * @param fileContent * @param options */ - async createNote(fileName: string, fileContent: string, options: CreateNoteOptions) { + async createNote(fileName: string, fileContent: string, options: CreateNoteOptions): Promise { // find and possibly create the folder set in settings or passed in folder - const folder = options.folder ?? this.app.vault.getAbstractFileByPath(this.settings.folder); - if (!folder) { - await this.app.vault.createFolder(this.settings.folder); - } + const folder = options.folder ?? this.app.vault.getAbstractFileByPath('/'); fileName = replaceIllegalFileNameCharactersInString(fileName); const filePath = `${folder.path}/${fileName}.md`; @@ -401,7 +403,7 @@ export default class MediaDbPlugin extends Plugin { * Update the active note by querying the API again. * Tries to read the type, id and dataSource of the active note. If successful it will query the api, delete the old note and create a new one. */ - async updateActiveNote(onlyMetadata: boolean = false) { + async updateActiveNote(onlyMetadata: boolean = false): Promise { const activeFile: TFile = this.app.workspace.getActiveFile(); if (!activeFile) { throw new Error('MDB | there is no active note'); @@ -416,7 +418,7 @@ export default class MediaDbPlugin extends Plugin { throw new Error('MDB | active note is not a Media DB entry or is missing metadata'); } - let oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(metadata, metadata.type); + const oldMediaTypeModel = this.mediaTypeManager.createMediaTypeModelFromMediaType(metadata, metadata.type); // console.debug(oldMediaTypeModel); let newMediaTypeModel = await this.apiManager.queryDetailedInfoById(metadata.id, metadata.dataSource); @@ -436,11 +438,11 @@ export default class MediaDbPlugin extends Plugin { } } - async createEntriesFromFolder(folder: TFolder) { + async createEntriesFromFolder(folder: TFolder): Promise { const erroredFiles: { filePath: string; error: string }[] = []; let canceled: boolean = false; - const { selectedAPI, titleFieldName, appendContent } = await new Promise<{ selectedAPI: string; titleFieldName: string; appendContent: boolean }>((resolve, reject) => { + const { selectedAPI, titleFieldName, appendContent } = await new Promise<{ selectedAPI: string; titleFieldName: string; appendContent: boolean }>(resolve => { new MediaDbFolderImportModal(this.app, this, (selectedAPI: string, titleFieldName: string, appendContent: boolean) => { resolve({ selectedAPI, titleFieldName, appendContent }); }).open(); @@ -454,11 +456,11 @@ export default class MediaDbPlugin extends Plugin { continue; } - let metadata: any = this.getMetadataFromFileCache(file); + const metadata: any = this.getMetadataFromFileCache(file); - let title = metadata[titleFieldName]; + const title = metadata[titleFieldName]; if (!title) { - erroredFiles.push({ filePath: file.path, error: `metadata field \'${titleFieldName}\' not found or empty` }); + erroredFiles.push({ filePath: file.path, error: `metadata field '${titleFieldName}' not found or empty` }); continue; } @@ -474,7 +476,7 @@ export default class MediaDbPlugin extends Plugin { continue; } - let { selectModalResult, selectModal } = await this.modalHelper.createSelectModal({ elements: results, skipButton: true, modalTitle: `Results for \'${title}\'` }); + const { selectModalResult, selectModal } = await this.modalHelper.createSelectModal({ elements: results, skipButton: true, modalTitle: `Results for '${title}'` }); if (selectModalResult.code === ModalResultCode.ERROR) { erroredFiles.push({ filePath: file.path, error: selectModalResult.error.message }); @@ -513,35 +515,34 @@ export default class MediaDbPlugin extends Plugin { } async createErroredFilesReport(erroredFiles: { filePath: string; error: string }[]): Promise { - const title = `bulk import error report ${dateTimeToString(new Date())}`; - const filePath = `${this.settings.folder.replace(/\/$/, '')}/${title}.md`; + const title = `MDB - bulk import error report ${dateTimeToString(new Date())}`; + const filePath = `${title}.md`; const table = [['file', 'error']].concat(erroredFiles.map(x => [x.filePath, x.error])); - let fileContent = `# ${title}\n\n${markdownTable(table)}`; - - const targetFile = await this.app.vault.create(filePath, fileContent); + const fileContent = `# ${title}\n\n${markdownTable(table)}`; + await this.app.vault.create(filePath, fileContent); } - async loadSettings() { + async loadSettings(): Promise { // console.log(DEFAULT_SETTINGS); const diskSettings: MediaDbPluginSettings = await this.loadData(); const defaultSettings: MediaDbPluginSettings = getDefaultSettings(this); const loadedSettings: MediaDbPluginSettings = Object.assign({}, defaultSettings, diskSettings); // migrate the settings loaded from the disk to match the structure of the default settings - let newPropertyMappings: PropertyMappingModel[] = []; + const newPropertyMappings: PropertyMappingModel[] = []; for (const defaultPropertyMappingModel of defaultSettings.propertyMappingModels) { - let newPropertyMappingModel: PropertyMappingModel = loadedSettings.propertyMappingModels.find(x => x.type === defaultPropertyMappingModel.type); + const newPropertyMappingModel: PropertyMappingModel = loadedSettings.propertyMappingModels.find(x => x.type === defaultPropertyMappingModel.type); if (newPropertyMappingModel === undefined) { // if the propertyMappingModel exists in the default settings but not the loaded settings, add it newPropertyMappings.push(defaultPropertyMappingModel); } else { // if the propertyMappingModel also exists in the loaded settings, add it from there - let newProperties: PropertyMapping[] = []; + const newProperties: PropertyMapping[] = []; for (const defaultProperty of defaultPropertyMappingModel.properties) { - let newProperty = newPropertyMappingModel.properties.find(x => x.property === defaultProperty.property); + const newProperty = newPropertyMappingModel.properties.find(x => x.property === defaultProperty.property); if (newProperty === undefined) { // default property is an instance newProperties.push(defaultProperty); @@ -559,8 +560,9 @@ export default class MediaDbPlugin extends Plugin { this.settings = loadedSettings; } - async saveSettings() { + async saveSettings(): Promise { this.mediaTypeManager.updateTemplates(this.settings); + this.mediaTypeManager.updateFolders(this.settings); await this.saveData(this.settings); } diff --git a/src/modals/MediaDbAdvancedSearchModal.ts b/src/modals/MediaDbAdvancedSearchModal.ts index 53536b6..9a79ac1 100644 --- a/src/modals/MediaDbAdvancedSearchModal.ts +++ b/src/modals/MediaDbAdvancedSearchModal.ts @@ -38,7 +38,7 @@ export class MediaDbAdvancedSearchModal extends Modal { this.closeCallback = closeCallback; } - keyPressCallback(event: KeyboardEvent) { + keyPressCallback(event: KeyboardEvent): void { if (event.key === 'Enter') { this.search(); } @@ -66,7 +66,7 @@ export class MediaDbAdvancedSearchModal extends Modal { } } - onOpen() { + onOpen(): void { const { contentEl } = this; contentEl.createEl('h2', { text: this.title }); @@ -123,7 +123,7 @@ export class MediaDbAdvancedSearchModal extends Modal { }); } - onClose() { + onClose(): void { this.closeCallback(); const { contentEl } = this; contentEl.empty(); diff --git a/src/modals/MediaDbFolderImportModal.ts b/src/modals/MediaDbFolderImportModal.ts index e36a4a5..04c908f 100644 --- a/src/modals/MediaDbFolderImportModal.ts +++ b/src/modals/MediaDbFolderImportModal.ts @@ -16,12 +16,12 @@ export class MediaDbFolderImportModal extends Modal { this.selectedApi = plugin.apiManager.apis[0].apiName; } - submit() { + submit(): void { this.onSubmit(this.selectedApi, this.titleFieldName, this.appendContent); this.close(); } - onOpen() { + onOpen(): void { const { contentEl } = this; contentEl.createEl('h2', { text: 'Import folder as Media DB entries' }); @@ -90,7 +90,7 @@ export class MediaDbFolderImportModal extends Modal { }); } - onClose() { + onClose(): void { const { contentEl } = this; contentEl.empty(); } diff --git a/src/modals/MediaDbIdSearchModal.ts b/src/modals/MediaDbIdSearchModal.ts index 9956c86..b100b46 100644 --- a/src/modals/MediaDbIdSearchModal.ts +++ b/src/modals/MediaDbIdSearchModal.ts @@ -33,7 +33,7 @@ export class MediaDbIdSearchModal extends Modal { this.closeCallback = closeCallback; } - keyPressCallback(event: KeyboardEvent) { + keyPressCallback(event: KeyboardEvent): void { if (event.key === 'Enter') { this.search(); } @@ -59,7 +59,7 @@ export class MediaDbIdSearchModal extends Modal { } } - onOpen() { + onOpen(): void { const { contentEl } = this; contentEl.createEl('h2', { text: this.title }); @@ -108,7 +108,7 @@ export class MediaDbIdSearchModal extends Modal { }); } - onClose() { + onClose(): void { this.closeCallback(); const { contentEl } = this; contentEl.empty(); diff --git a/src/modals/MediaDbPreviewModal.ts b/src/modals/MediaDbPreviewModal.ts index ea26b34..d9576c8 100644 --- a/src/modals/MediaDbPreviewModal.ts +++ b/src/modals/MediaDbPreviewModal.ts @@ -40,14 +40,14 @@ export class MediaDbPreviewModal extends Modal { } async preview(): Promise { - let { contentEl } = this; + const { contentEl } = this; contentEl.addClass('media-db-plugin-preview-modal'); contentEl.createEl('h2', { text: this.title }); const previewWrapper = contentEl.createDiv({ cls: 'media-db-plugin-preview-wrapper' }); - for (let result of this.elements) { + for (const result of this.elements) { previewWrapper.createEl('h3', { text: result.englishTitle }); const fileDiv = previewWrapper.createDiv(); diff --git a/src/modals/MediaDbSearchModal.ts b/src/modals/MediaDbSearchModal.ts index b878371..b4c89a9 100644 --- a/src/modals/MediaDbSearchModal.ts +++ b/src/modals/MediaDbSearchModal.ts @@ -1,16 +1,10 @@ import { ButtonComponent, Modal, Notice, Setting, TextComponent, ToggleComponent } from 'obsidian'; import { MediaTypeModel } from '../models/MediaTypeModel'; import MediaDbPlugin from '../main'; -import { - ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS, - AdvancedSearchModalData, - AdvancedSearchModalOptions, - SEARCH_MODAL_DEFAULT_OPTIONS, - SearchModalData, - SearchModalOptions, -} from '../utils/ModalHelper'; +import { SEARCH_MODAL_DEFAULT_OPTIONS, SearchModalData, SearchModalOptions } from '../utils/ModalHelper'; import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { unCamelCase } from '../utils/Utils'; +import { MediaType } from '../utils/MediaType'; export class MediaDbSearchModal extends Modal { plugin: MediaDbPlugin; @@ -18,7 +12,7 @@ export class MediaDbSearchModal extends Modal { query: string; isBusy: boolean; title: string; - selectedTypes: { name: string; selected: boolean }[]; + selectedTypes: { name: MediaType; selected: boolean }[]; searchBtn: ButtonComponent; @@ -47,7 +41,7 @@ export class MediaDbSearchModal extends Modal { this.closeCallback = closeCallback; } - keyPressCallback(event: KeyboardEvent) { + keyPressCallback(event: KeyboardEvent): void { if (event.key === 'Enter') { this.search(); } @@ -59,7 +53,7 @@ export class MediaDbSearchModal extends Modal { return; } - const types: string[] = this.selectedTypes.filter(x => x.selected).map(x => x.name); + const types: MediaType[] = this.selectedTypes.filter(x => x.selected).map(x => x.name); if (types.length === 0) { new Notice('MDB | No Type selected'); @@ -75,7 +69,7 @@ export class MediaDbSearchModal extends Modal { } } - onOpen() { + onOpen(): void { const { contentEl } = this; contentEl.createEl('h2', { text: this.title }); @@ -130,7 +124,7 @@ export class MediaDbSearchModal extends Modal { }); } - onClose() { + onClose(): void { this.closeCallback(); const { contentEl } = this; contentEl.empty(); diff --git a/src/modals/MediaDbSearchResultModal.ts b/src/modals/MediaDbSearchResultModal.ts index 03e3d48..57c12f8 100644 --- a/src/modals/MediaDbSearchResultModal.ts +++ b/src/modals/MediaDbSearchResultModal.ts @@ -40,14 +40,14 @@ export class MediaDbSearchResultModal extends SelectModal { } // Renders each suggestion item. - renderElement(item: MediaTypeModel, el: HTMLElement) { + renderElement(item: MediaTypeModel, el: HTMLElement): void { el.createEl('div', { text: this.plugin.mediaTypeManager.getFileName(item) }); el.createEl('small', { text: `${item.getSummary()}\n` }); el.createEl('small', { text: `${item.type.toUpperCase() + (item.subType ? ` (${item.subType})` : '')} from ${item.dataSource}` }); } // Perform action on the selected suggestion. - submit() { + submit(): void { if (!this.busy) { this.busy = true; this.submitButton.setButtonText('Creating entry...'); @@ -55,12 +55,12 @@ export class MediaDbSearchResultModal extends SelectModal { } } - skip() { + skip(): void { this.skipButton.setButtonText('Skipping...'); this.skipCallback(); } - onClose() { + onClose(): void { this.closeCallback(); } } diff --git a/src/modals/SelectModal.ts b/src/modals/SelectModal.ts index e052e05..dd89d52 100644 --- a/src/modals/SelectModal.ts +++ b/src/modals/SelectModal.ts @@ -59,7 +59,7 @@ export abstract class SelectModal extends Modal { abstract skip(): void; - disableAllOtherElements(elementId: number) { + disableAllOtherElements(elementId: number): void { for (const selectModalElement of this.selectModalElements) { if (selectModalElement.id !== elementId) { selectModalElement.setActive(false); @@ -67,7 +67,7 @@ export abstract class SelectModal extends Modal { } } - deHighlightAllOtherElements(elementId: number) { + deHighlightAllOtherElements(elementId: number): void { for (const selectModalElement of this.selectModalElements) { if (selectModalElement.id !== elementId) { selectModalElement.setHighlighted(false); @@ -75,7 +75,7 @@ export abstract class SelectModal extends Modal { } } - async onOpen() { + async onOpen(): Promise { const { contentEl, titleEl } = this; titleEl.createEl('h2', { text: this.title }); @@ -122,7 +122,7 @@ export abstract class SelectModal extends Modal { }); } - activateHighlighted() { + activateHighlighted(): void { for (const selectModalElement of this.selectModalElements) { if (selectModalElement.isHighlighted()) { selectModalElement.setActive(!selectModalElement.isActive()); @@ -133,7 +133,7 @@ export abstract class SelectModal extends Modal { } } - highlightUp() { + highlightUp(): void { for (const selectModalElement of this.selectModalElements) { if (selectModalElement.isHighlighted()) { this.getPreviousSelectModalElement(selectModalElement).setHighlighted(true); @@ -145,7 +145,7 @@ export abstract class SelectModal extends Modal { this.selectModalElements.last().setHighlighted(true); } - highlightDown() { + highlightDown(): void { for (const selectModalElement of this.selectModalElements) { if (selectModalElement.isHighlighted()) { this.getNextSelectModalElement(selectModalElement).setHighlighted(true); diff --git a/src/modals/SelectModalElement.ts b/src/modals/SelectModalElement.ts index 28047d5..9b661b7 100644 --- a/src/modals/SelectModalElement.ts +++ b/src/modals/SelectModalElement.ts @@ -45,7 +45,7 @@ export class SelectModalElement { return this.highlighted; } - setHighlighted(value: boolean) { + setHighlighted(value: boolean): void { this.highlighted = value; if (this.highlighted) { this.addClass(this.hoverClass); diff --git a/src/models/BoardGameModel.ts b/src/models/BoardGameModel.ts index cb53a25..70b86e5 100644 --- a/src/models/BoardGameModel.ts +++ b/src/models/BoardGameModel.ts @@ -5,6 +5,11 @@ import { MediaType } from '../utils/MediaType'; export class BoardGameModel extends MediaTypeModel { genres: string[]; onlineRating: number; + complexityRating: number; + minPlayers: number; + maxPlayers: number; + playtime: string; + publishers: string[]; image?: string; released: boolean; @@ -19,8 +24,15 @@ export class BoardGameModel extends MediaTypeModel { this.genres = undefined; this.onlineRating = undefined; + this.minPlayers = undefined; + this.maxPlayers = undefined; + this.playtime = undefined; + this.publishers = undefined; + this.complexityRating = undefined; this.image = undefined; + this.released = undefined; + this.userData = { played: undefined, personalRating: undefined, diff --git a/src/models/MovieModel.ts b/src/models/MovieModel.ts index 5c99863..83d874f 100644 --- a/src/models/MovieModel.ts +++ b/src/models/MovieModel.ts @@ -7,9 +7,11 @@ export class MovieModel extends MediaTypeModel { producer: string; duration: string; onlineRating: number; + actors: string[]; image: string; released: boolean; + streamingServices: string[]; premiere: string; userData: { @@ -25,9 +27,13 @@ export class MovieModel extends MediaTypeModel { this.producer = undefined; this.duration = undefined; this.onlineRating = undefined; + this.actors = undefined; this.image = undefined; + this.released = undefined; + this.streamingServices = undefined; this.premiere = undefined; + this.userData = { watched: undefined, lastWatched: undefined, diff --git a/src/models/SeriesModel.ts b/src/models/SeriesModel.ts index 8df01cc..5132f1e 100644 --- a/src/models/SeriesModel.ts +++ b/src/models/SeriesModel.ts @@ -17,9 +17,11 @@ export class SeriesModel extends MediaTypeModel { episodes: number; duration: string; onlineRating: number; + actors: string[]; image: string; released: boolean; + streamingServices: string[]; airing: boolean; airedFrom: string; airedTo: string; @@ -38,11 +40,15 @@ export class SeriesModel extends MediaTypeModel { this.episodes = undefined; this.duration = undefined; this.onlineRating = undefined; + this.actors = undefined; this.image = undefined; + this.released = undefined; + this.streamingServices = undefined; this.airing = undefined; this.airedFrom = undefined; this.airedTo = undefined; + this.userData = { watched: undefined, lastWatched: undefined, diff --git a/src/settings/PropertyMapper.ts b/src/settings/PropertyMapper.ts index 40f24ca..1ca6a28 100644 --- a/src/settings/PropertyMapper.ts +++ b/src/settings/PropertyMapper.ts @@ -40,6 +40,7 @@ export class PropertyMapper { // @ts-ignore newObj[propertyMapping.newProperty] = value; } else if (propertyMapping.mapping === PropertyMappingOption.Remove) { + // do nothing } else if (propertyMapping.mapping === PropertyMappingOption.Default) { // @ts-ignore newObj[key] = value; diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 37f17af..4c37060 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -9,7 +9,6 @@ import { MEDIA_TYPES } from '../utils/MediaTypeManager'; import { MediaTypeModel } from '../models/MediaTypeModel'; export interface MediaDbPluginSettings { - folder: string; OMDbKey: string; sfwFilter: boolean; useCustomYamlStringifier: boolean; @@ -36,11 +35,17 @@ export interface MediaDbPluginSettings { musicReleasePropertyConversionRules: string; boardgamePropertyConversionRules: string; + movieFolder: string; + seriesFolder: string; + gameFolder: string; + wikiFolder: string; + musicReleaseFolder: string; + boardgameFolder: string; + propertyMappingModels: PropertyMappingModel[]; } const DEFAULT_SETTINGS: MediaDbPluginSettings = { - folder: 'Media DB', OMDbKey: '', sfwFilter: true, useCustomYamlStringifier: true, @@ -67,13 +72,20 @@ const DEFAULT_SETTINGS: MediaDbPluginSettings = { musicReleasePropertyConversionRules: '', boardgamePropertyConversionRules: '', + movieFolder: 'Media DB/movies', + seriesFolder: 'Media DB/series', + gameFolder: 'Media DB/games', + wikiFolder: 'Media DB/wiki', + musicReleaseFolder: 'Media DB/music', + boardgameFolder: 'Media DB/boardgames', + propertyMappingModels: [], }; export const lockedPropertyMappings: string[] = ['type', 'id', 'dataSource']; export function getDefaultSettings(plugin: MediaDbPlugin): MediaDbPluginSettings { - let defaultSettings = DEFAULT_SETTINGS; + const defaultSettings = DEFAULT_SETTINGS; // construct property mapping defaults const propertyMappingModels: PropertyMappingModel[] = []; @@ -111,19 +123,6 @@ export class MediaDbSettingTab extends PluginSettingTab { containerEl.createEl('h2', { text: 'Media DB Plugin Settings' }); - new Setting(containerEl) - .setName('New file location') - .setDesc('New media db entries will be placed here.') - .addSearch(cb => { - new FolderSuggest(this.app, cb.inputEl); - cb.setPlaceholder('Example: folder1/folder2') - .setValue(this.plugin.settings.folder) - .onChange(data => { - this.plugin.settings.folder = data; - this.plugin.saveSettings(); - }); - }); - new Setting(containerEl) .setName('OMDb API key') .setDesc('API key for "www.omdbapi.com".') @@ -166,6 +165,87 @@ export class MediaDbSettingTab extends PluginSettingTab { }); }); + containerEl.createEl('h3', { text: 'New File Location' }); + // region new file location + new Setting(containerEl) + .setName('Movie Folder') + .setDesc('Where newly imported movies should be places.') + .addSearch(cb => { + new FolderSuggest(this.app, cb.inputEl); + cb.setPlaceholder(DEFAULT_SETTINGS.movieFolder) + .setValue(this.plugin.settings.movieFolder) + .onChange(data => { + this.plugin.settings.movieFolder = data; + this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName('Series Folder') + .setDesc('Where newly imported series should be places.') + .addSearch(cb => { + new FolderSuggest(this.app, cb.inputEl); + cb.setPlaceholder(DEFAULT_SETTINGS.seriesFolder) + .setValue(this.plugin.settings.seriesFolder) + .onChange(data => { + this.plugin.settings.seriesFolder = data; + this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName('Game Folder') + .setDesc('Where newly imported games should be places.') + .addSearch(cb => { + new FolderSuggest(this.app, cb.inputEl); + cb.setPlaceholder(DEFAULT_SETTINGS.gameFolder) + .setValue(this.plugin.settings.gameFolder) + .onChange(data => { + this.plugin.settings.gameFolder = data; + this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName('Wiki Folder') + .setDesc('Where newly imported wiki articles should be places.') + .addSearch(cb => { + new FolderSuggest(this.app, cb.inputEl); + cb.setPlaceholder(DEFAULT_SETTINGS.wikiFolder) + .setValue(this.plugin.settings.wikiFolder) + .onChange(data => { + this.plugin.settings.wikiFolder = data; + this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName('Music Folder') + .setDesc('Where newly imported music should be places.') + .addSearch(cb => { + new FolderSuggest(this.app, cb.inputEl); + cb.setPlaceholder(DEFAULT_SETTINGS.musicReleaseFolder) + .setValue(this.plugin.settings.musicReleaseFolder) + .onChange(data => { + this.plugin.settings.musicReleaseFolder = data; + this.plugin.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName('Board Game Folder') + .setDesc('Where newly imported board games should be places.') + .addSearch(cb => { + new FileSuggest(this.app, cb.inputEl); + cb.setPlaceholder(DEFAULT_SETTINGS.boardgameFolder) + .setValue(this.plugin.settings.boardgameFolder) + .onChange(data => { + this.plugin.settings.boardgameFolder = data; + this.plugin.saveSettings(); + }); + }); + // endregion + containerEl.createEl('h3', { text: 'Template Settings' }); // region templates new Setting(containerEl) @@ -326,7 +406,7 @@ export class MediaDbSettingTab extends PluginSettingTab { containerEl.createEl('h3', { text: 'Property Mappings' }); - let propertyMappingExplanation = containerEl.createEl('div'); + const propertyMappingExplanation = containerEl.createEl('div'); propertyMappingExplanation.innerHTML = `

Allow you to remap the metadata fields of newly created media db entries.

@@ -345,8 +425,8 @@ export class MediaDbSettingTab extends PluginSettingTab { target: this.containerEl, props: { models: this.plugin.settings.propertyMappingModels.map(x => x.copy()), - save: (model: PropertyMappingModel) => { - let propertyMappingModels: PropertyMappingModel[] = []; + save: (model: PropertyMappingModel): void => { + const propertyMappingModels: PropertyMappingModel[] = []; for (const model2 of this.plugin.settings.propertyMappingModels) { if (model2.type === model.type) { diff --git a/src/settings/suggesters/Suggest.ts b/src/settings/suggesters/Suggest.ts index 3e9e93a..331db6b 100644 --- a/src/settings/suggesters/Suggest.ts +++ b/src/settings/suggesters/Suggest.ts @@ -53,7 +53,7 @@ export class Suggest { this.setSelectedItem(item, false); } - setSuggestions(values: T[]) { + setSuggestions(values: T[]): void { this.containerEl.empty(); const suggestionEls: HTMLDivElement[] = []; @@ -68,14 +68,14 @@ export class Suggest { this.setSelectedItem(0, false); } - useSelectedItem(event: MouseEvent | KeyboardEvent) { + useSelectedItem(event: MouseEvent | KeyboardEvent): void { const currentValue = this.values[this.selectedItem]; if (currentValue) { this.owner.selectSuggestion(currentValue, event); } } - setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { + setSelectedItem(selectedIndex: number, scrollIntoView: boolean): void { const normalizedIndex = this.suggestions.length > 0 ? wrapAround(selectedIndex, this.suggestions.length) : 0; const prevSelectedSuggestion = this.suggestions[this.selectedItem]; const selectedSuggestion = this.suggestions[normalizedIndex]; @@ -141,7 +141,7 @@ export abstract class TextInputSuggest implements ISuggestOwner { { name: 'sameWidth', enabled: true, - fn: ({ state, instance }) => { + fn: ({ state, instance }): void => { // Note: positioning needs to be calculated twice - // first pass - positioning it according to the width of the popper // second pass - position it with the width bound to the reference element diff --git a/src/utils/MediaTypeManager.ts b/src/utils/MediaTypeManager.ts index 22f5140..05bc4a3 100644 --- a/src/utils/MediaTypeManager.ts +++ b/src/utils/MediaTypeManager.ts @@ -2,7 +2,7 @@ import { MediaDbPluginSettings } from '../settings/Settings'; import { MediaType } from './MediaType'; import { MediaTypeModel } from '../models/MediaTypeModel'; import { replaceTags } from './Utils'; -import { App, TFile } from 'obsidian'; +import { App, TAbstractFile, TFile, TFolder } from 'obsidian'; import { MovieModel } from '../models/MovieModel'; import { SeriesModel } from '../models/SeriesModel'; import { GameModel } from '../models/GameModel'; @@ -15,10 +15,11 @@ export const MEDIA_TYPES: MediaType[] = [MediaType.Movie, MediaType.Series, Medi export class MediaTypeManager { mediaFileNameTemplateMap: Map; mediaTemplateMap: Map; + mediaFolderMap: Map; constructor() {} - updateTemplates(settings: MediaDbPluginSettings) { + updateTemplates(settings: MediaDbPluginSettings): void { this.mediaFileNameTemplateMap = new Map(); this.mediaFileNameTemplateMap.set(MediaType.Movie, settings.movieFileNameTemplate); this.mediaFileNameTemplateMap.set(MediaType.Series, settings.seriesFileNameTemplate); @@ -36,11 +37,21 @@ export class MediaTypeManager { this.mediaTemplateMap.set(MediaType.BoardGame, settings.boardgameTemplate); } + updateFolders(settings: MediaDbPluginSettings): void { + this.mediaFolderMap = new Map(); + this.mediaFolderMap.set(MediaType.Movie, settings.movieFolder); + this.mediaFolderMap.set(MediaType.Series, settings.seriesFolder); + this.mediaFolderMap.set(MediaType.Game, settings.gameFolder); + this.mediaFolderMap.set(MediaType.Wiki, settings.wikiFolder); + this.mediaFolderMap.set(MediaType.MusicRelease, settings.musicReleaseFolder); + this.mediaFolderMap.set(MediaType.BoardGame, settings.boardgameFolder); + } + getFileName(mediaTypeModel: MediaTypeModel): string { return replaceTags(this.mediaFileNameTemplateMap.get(mediaTypeModel.getMediaType()), mediaTypeModel); } - async getTemplate(mediaTypeModel: MediaTypeModel, app: App) { + async getTemplate(mediaTypeModel: MediaTypeModel, app: App): Promise { const templateFileName = this.mediaTemplateMap.get(mediaTypeModel.getMediaType()); if (!templateFileName) { @@ -61,6 +72,26 @@ export class MediaTypeManager { return replaceTags(template, mediaTypeModel); } + async getFolder(mediaTypeModel: MediaTypeModel, app: App): Promise { + let folderPath = this.mediaFolderMap.get(mediaTypeModel.getMediaType()); + + if (!folderPath) { + folderPath = `/`; + } + console.log(folderPath); + + if (!(await app.vault.adapter.exists(folderPath))) { + await app.vault.createFolder(folderPath); + } + const folder: TAbstractFile = app.vault.getAbstractFileByPath(folderPath); + + if (!(folder instanceof TFolder)) { + throw Error(`Expected ${folder} to be instance of TFolder`); + } + + return folder; + } + /** * Takes an object and a MediaType and turns the object into an instance of a MediaTypeModel corresponding to the MediaType passed in. * diff --git a/src/utils/ModalHelper.ts b/src/utils/ModalHelper.ts index 24e7217..808aa5e 100644 --- a/src/utils/ModalHelper.ts +++ b/src/utils/ModalHelper.ts @@ -7,6 +7,7 @@ import MediaDbPlugin from '../main'; import { MediaDbPreviewModal } from 'src/modals/MediaDbPreviewModal'; import { CreateNoteOptions } from './Utils'; import { MediaDbSearchModal } from '../modals/MediaDbSearchModal'; +import { MediaType } from './MediaType'; export enum ModalResultCode { SUCCESS, @@ -77,7 +78,7 @@ export interface PreviewModalResult { */ export interface SearchModalData { query: string; - types: string[]; + types: MediaType[]; } /** @@ -124,7 +125,7 @@ export interface PreviewModalData { */ export interface SearchModalOptions { modalTitle?: string; - preselectedTypes?: string[]; + preselectedTypes?: MediaType[]; prefilledSearchString?: string; } @@ -228,7 +229,7 @@ export class ModalHelper { */ async createSearchModal(searchModalOptions: SearchModalOptions): Promise<{ searchModalResult: SearchModalResult; searchModal: MediaDbSearchModal }> { const modal = new MediaDbSearchModal(this.plugin, searchModalOptions); - const res: SearchModalResult = await new Promise((resolve, reject) => { + const res: SearchModalResult = await new Promise(resolve => { modal.setSubmitCallback(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); modal.setCloseCallback(err => { if (err) { @@ -268,8 +269,7 @@ export class ModalHelper { } try { - let callbackRes: MediaTypeModel[]; - callbackRes = await submitCallback(searchModalResult.data); + const callbackRes: MediaTypeModel[] = await submitCallback(searchModalResult.data); searchModal.close(); return callbackRes; } catch (e) { @@ -292,7 +292,7 @@ export class ModalHelper { advancedSearchModalOptions: AdvancedSearchModalOptions ): Promise<{ advancedSearchModalResult: AdvancedSearchModalResult; advancedSearchModal: MediaDbAdvancedSearchModal }> { const modal = new MediaDbAdvancedSearchModal(this.plugin, advancedSearchModalOptions); - const res: AdvancedSearchModalResult = await new Promise((resolve, reject) => { + const res: AdvancedSearchModalResult = await new Promise(resolve => { modal.setSubmitCallback(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); modal.setCloseCallback(err => { if (err) { @@ -335,8 +335,7 @@ export class ModalHelper { } try { - let callbackRes: MediaTypeModel[]; - callbackRes = await submitCallback(advancedSearchModalResult.data); + const callbackRes: MediaTypeModel[] = await submitCallback(advancedSearchModalResult.data); advancedSearchModal.close(); return callbackRes; } catch (e) { @@ -357,7 +356,7 @@ export class ModalHelper { */ async createIdSearchModal(idSearchModalOptions: IdSearchModalOptions): Promise<{ idSearchModalResult: IdSearchModalResult; idSearchModal: MediaDbIdSearchModal }> { const modal = new MediaDbIdSearchModal(this.plugin, idSearchModalOptions); - const res: IdSearchModalResult = await new Promise((resolve, reject) => { + const res: IdSearchModalResult = await new Promise(resolve => { modal.setSubmitCallback(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); modal.setCloseCallback(err => { if (err) { @@ -400,8 +399,7 @@ export class ModalHelper { } try { - let callbackRes: MediaTypeModel; - callbackRes = await submitCallback(idSearchModalResult.data); + const callbackRes: MediaTypeModel = await submitCallback(idSearchModalResult.data); idSearchModal.close(); return callbackRes; } catch (e) { @@ -422,7 +420,7 @@ export class ModalHelper { */ async createSelectModal(selectModalOptions: SelectModalOptions): Promise<{ selectModalResult: SelectModalResult; selectModal: MediaDbSearchResultModal }> { const modal = new MediaDbSearchResultModal(this.plugin, selectModalOptions); - const res: SelectModalResult = await new Promise((resolve, reject) => { + const res: SelectModalResult = await new Promise(resolve => { modal.setSubmitCallback(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); modal.setSkipCallback(() => resolve({ code: ModalResultCode.SKIP })); modal.setCloseCallback(err => { @@ -468,8 +466,7 @@ export class ModalHelper { } try { - let callbackRes: MediaTypeModel[]; - callbackRes = await submitCallback(selectModalResult.data); + const callbackRes: MediaTypeModel[] = await submitCallback(selectModalResult.data); selectModal.close(); return callbackRes; } catch (e) { @@ -483,7 +480,7 @@ export class ModalHelper { async createPreviewModal(previewModalOptions: PreviewModalOptions): Promise<{ previewModalResult: PreviewModalResult; previewModal: MediaDbPreviewModal }> { //todo: handle attachFile for existing files const modal = new MediaDbPreviewModal(this.plugin, previewModalOptions); - const res: PreviewModalResult = await new Promise((resolve, reject) => { + const res: PreviewModalResult = await new Promise(resolve => { modal.setSubmitCallback(res => resolve({ code: ModalResultCode.SUCCESS, data: res })); modal.setCloseCallback(err => { if (err) { @@ -515,8 +512,7 @@ export class ModalHelper { } try { - let callbackRes: boolean; - callbackRes = await submitCallback(previewModalResult.data); + const callbackRes: boolean = await submitCallback(previewModalResult.data); previewModal.close(); return callbackRes; } catch (e) { diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 6220759..c4e4333 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -4,7 +4,7 @@ import { TFile, TFolder } from 'obsidian'; export const pluginName: string = 'obsidian-media-db-plugin'; export const contactEmail: string = 'm.projects.code@gmail.com'; export const mediaDbTag: string = 'mediaDB'; -export const mediaDbVersion: string = '0.4.1'; +export const mediaDbVersion: string = '0.5.0'; export const debug: boolean = true; export function wrapAround(value: number, size: number): number { @@ -14,16 +14,6 @@ export function wrapAround(value: number, size: number): number { return mod(value, size); } -/** - * @deprecated use console.debug instead - * @param o - */ -export function debugLog(o: any): void { - if (debug) { - console.log(o); - } -} - export function containsOnlyLettersAndUnderscores(str: string): boolean { return /^[a-zA-Z_]+$/.test(str); } @@ -33,9 +23,7 @@ export function replaceIllegalFileNameCharactersInString(string: string): string } export function replaceTags(template: string, mediaTypeModel: MediaTypeModel): string { - const resolvedTemplate = template.replace(new RegExp('{{.*?}}', 'g'), (match: string) => replaceTag(match, mediaTypeModel)); - - return resolvedTemplate; + return template.replace(new RegExp('{{.*?}}', 'g'), (match: string) => replaceTag(match, mediaTypeModel)); } function replaceTag(match: string, mediaTypeModel: MediaTypeModel): string { @@ -44,11 +32,11 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel): string { tag = tag.substring(0, tag.length - 2); tag = tag.trim(); - let parts = tag.split(':'); + const parts = tag.split(':'); if (parts.length === 1) { - let path = parts[0].split('.'); + const path = parts[0].split('.'); - let obj = traverseMetaData(path, mediaTypeModel); + const obj = traverseMetaData(path, mediaTypeModel); if (obj === undefined) { return '{{ INVALID TEMPLATE TAG - object undefined }}'; @@ -56,11 +44,11 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel): string { return obj; } else if (parts.length === 2) { - let operator = parts[0]; + const operator = parts[0]; - let path = parts[1].split('.'); + const path = parts[1].split('.'); - let obj = traverseMetaData(path, mediaTypeModel); + const obj = traverseMetaData(path, mediaTypeModel); if (obj === undefined) { return '{{ INVALID TEMPLATE TAG - object undefined }}'; @@ -87,7 +75,7 @@ function replaceTag(match: string, mediaTypeModel: MediaTypeModel): string { function traverseMetaData(path: Array, mediaTypeModel: MediaTypeModel): any { let o: any = mediaTypeModel; - for (let part of path) { + for (const part of path) { if (o !== undefined) { o = o[part]; } @@ -97,12 +85,12 @@ function traverseMetaData(path: Array, mediaTypeModel: MediaTypeModel): } export function markdownTable(content: string[][]): string { - let rows = content.length; + const rows = content.length; if (rows === 0) { return ''; } - let columns = content[0].length; + const columns = content[0].length; if (columns === 0) { return ''; } @@ -112,7 +100,7 @@ export function markdownTable(content: string[][]): string { } } - let longestStringInColumns: number[] = []; + const longestStringInColumns: number[] = []; for (let i = 0; i < columns; i++) { let longestStringInColumn = 0; @@ -147,36 +135,18 @@ export function markdownTable(content: string[][]): string { return table; } -export function dateToString(date: Date) { +export function dateToString(date: Date): string { return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`; } -export function timeToString(time: Date) { +export function timeToString(time: Date): string { return `${time.getHours()}-${time.getMinutes()}-${time.getSeconds()}`; } -export function dateTimeToString(dateTime: Date) { +export function dateTimeToString(dateTime: Date): string { return `${dateToString(dateTime)} ${timeToString(dateTime)}`; } -/** - * @deprecated - */ -export class UserCancelError extends Error { - constructor(message: string) { - super(message); - } -} - -/** - * @deprecated - */ -export class UserSkipError extends Error { - constructor(message: string) { - super(message); - } -} - // js can't even implement modulo correctly... export function mod(n: number, m: number): number { return ((n % m) + m) % m; @@ -202,6 +172,7 @@ export class PropertyMappingNameConflictError extends Error { * - attachTemplate: whether to attach the template (DEFAULT: false) * - attachFie: a file to attach (DEFAULT: undefined) * - openNote: whether to open the note after creation (DEFAULT: false) + * - folder: folder to put the note in */ export interface CreateNoteOptions { attachTemplate?: boolean;