Merge pull request #28 from alexankitty/metadata
Adds Metadata to search Revamps search Moves old table search to its own setting Removes sorting on old table search as it doesn't really function anymore Embeds old setting in the url so results are identical when sharing links Adds metadata about information Updates translations Adds an info page that will give a summary of the game, when it released, pictures, videos, gamemodes, genre, etc Added image gallery partial with youtube video embed support Adds two new environment variables for IGDB auth Automatically disables the new result interface if metadata db is empty. Adds regional cover support. Adds locale title support. Implements a task to pull all applicable metadata from IGDB. Implements full text search on metadata titles. Adds backup fuzzy search to match as many games to metadata as possible. Provides additional information on task statuses in console Moves db optimization task to be handled after metadata Adds IGDB mappings to keep platform names consistent Adds locale name to code mapping. Makes fuzzy search match run after main metadata match operation so that it doesn't delay the other database tasks unnecessarily Adds badges to point out which search experience the user is currently using. Clicking directs to the settings page where they can change their search experience. Fix suggestions by normalizing the names, and then deduplicating the results before sending them to the client
8
.env
@@ -2,7 +2,7 @@ PORT=8062
|
||||
BIND_ADDRESS=0.0.0.0
|
||||
FORCE_FILE_REBUILD=0
|
||||
DEBUG=0
|
||||
NODE_ENV=production
|
||||
NODE_ENV=dev
|
||||
# Memory Impacting Settings - Trades for threading efficiency. Much slower, but should be useful for limited memory environments like VPS
|
||||
# May also decrease 504 failure rates
|
||||
# Changes the maximum number of jobs the crawler can queue. Setting it too high will cause a call stack overflow
|
||||
@@ -13,6 +13,8 @@ MAX_FETCH_JOBS=1000
|
||||
INSTANCE_NAME=Myrient
|
||||
# Enable the built-in emulator
|
||||
EMULATOR_ENABLED=true
|
||||
# Set the hostname
|
||||
HOSTNAME=myrient.mahou.one
|
||||
|
||||
# Run docker-compose.dev.yml for running locally
|
||||
# Database Configuration
|
||||
@@ -24,3 +26,7 @@ POSTGRES_PASSWORD=development
|
||||
|
||||
# Elasticsearch Configuration
|
||||
ELASTICSEARCH_URL=http://localhost:9200
|
||||
|
||||
#IGDB Connection Configuration - Not setting this will disable the new search page and metadata pull
|
||||
TWITCH_CLIENT_ID=
|
||||
TWITCH_CLIENT_SECRET=
|
||||
|
||||
@@ -6,6 +6,9 @@ It is finally here. There is now a way to search all of Myrient's offerings.
|
||||
- 800MB-ish of memory for running the server
|
||||
- HTTPS for some CORS functions to work correctly.
|
||||
|
||||
# Post Metasearch update
|
||||
Please clear your elasticsearch instance, and possibly run a new file rebuild to ensure there are no errors after updating your docker container or pulling the repo.
|
||||
|
||||
# Self-Hosting
|
||||
|
||||
## Docker Method (Recommended)
|
||||
@@ -69,6 +72,12 @@ server {
|
||||
For the SSL certificate you can use certbot via the `certbot -d servername.tld` command and adding it to your `crontab`.
|
||||
[Additional Information for Certbot Setup](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-20-04)
|
||||
|
||||
# Opengraph
|
||||
To ensure OpenGraph metadata embed for chat apps works correctly, please be sure to set `HOSTNAME` in `.env` or `docker-compose.yml` to the FQDN (fully qualifed domain name) of the server that is hosting the site.
|
||||
|
||||
# Metadata
|
||||
To enable metadata synchronize and matching, you will need to create a developer application in the [Twitch TV Developer Console](https://dev.twitch.tv/console) and then add your client id to `TWITCH_CLIENT_ID` in `.env` or `docker-compose.yml` along with adding your client secret to `TWITCH_CLIENT_SECRET`. Metadata takes about half an hour to synchronize from IGDB to your database, and about another half an hour to match via Postgres Full Text Search. Once all other database maintenance operations are done, the database will attempt to match anything that still isn't matched using a much slower fuzzy trigram search that can take up to a day to complete. These processes won't run again until a new crawl of myrient has been performed and the file count has increased.
|
||||
|
||||
# Contributing
|
||||
You know the usual fluff.
|
||||
Is there a missing category or string association? `lib/categories.json` and any of the files under `lib/json/relatedkeywords` can both updated to include these. If you do update/improve these, please put in a pull request so that it can be added to the public hosted server, as well.
|
||||
|
||||
@@ -24,6 +24,7 @@ console.log('Available locales:', availableLocales);
|
||||
i18n.configure({
|
||||
locales: availableLocales,
|
||||
defaultLocale: 'en',
|
||||
retryInDefaultLocale: true,
|
||||
directory: localesDir,
|
||||
objectNotation: true,
|
||||
updateFiles: false,
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "في {{seconds}} ثانية",
|
||||
"indexing": "جارٍ الفهرسة، إذا كان هناك شيء مفقود في القائمة، يرجى المحاولة مرة أخرى بعد بضع دقائق",
|
||||
"non_game_filter": "تم تفعيل فلتر المحتوى غير اللعبي",
|
||||
"displaying_results": "عرض النتائج من {{start}} إلى {{end}}."
|
||||
"displaying_results": "عرض النتائج من {{start}} إلى {{end}}.",
|
||||
"no_description": "لم يتم العثور على وصف.",
|
||||
"no_metadata": "لم يتم العثور على بيانات وصفية.",
|
||||
"released": "تم الإصدار:",
|
||||
"release_date": "تاريخ الإصدار:",
|
||||
"region": "المنطقة:",
|
||||
"platform": "المنصة:",
|
||||
"genre": "النوع:",
|
||||
"published": "الناشر:",
|
||||
"developed": "المطور:",
|
||||
"modes": "أنماط اللعب:",
|
||||
"download": "تحميل",
|
||||
"filename": "اسم الملف:",
|
||||
"release_group": "مجموعة الإصدار:",
|
||||
"upload_date": "تاريخ الرفع:",
|
||||
"more_info": "المزيد من المعلومات",
|
||||
"size": "الحجم:",
|
||||
"old_experience": "استخدام تجربة البحث القديمة.",
|
||||
"new_experience": "استخدام تجربة البحث الجديدة."
|
||||
},
|
||||
"about": {
|
||||
"title": "حول الموقع",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "تم إنشاء محرك البحث بواسطة",
|
||||
"view_github": "عرض المشروع على GitHub"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "معلومات البيانات الوصفية",
|
||||
"description": "يقوم هذا الموقع بجلب معلومات البيانات الوصفية عن الألعاب من {{metadata_source}}. قد تكون بعض البيانات الوصفية مفقودة أو غير صحيحة بسبب عدم تطابق اسم ROM أو مشاكل مع مزود الخدمة."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -67,8 +89,12 @@
|
||||
"tooltip": "يتطلب تطابق جميع الكلمات في استعلام البحث."
|
||||
},
|
||||
"hide_non_game": {
|
||||
"label": "إخفاء المحتوى غير اللعبي",
|
||||
"tooltip": "يفلتر تعديلات الـ ROM، والتصحيحات، والرسومات، والمحتويات الأخرى غير اللعبية من نتائج البحث."
|
||||
"label": "إخفاء المحتوى غير المتعلق بالألعاب",
|
||||
"tooltip": "يقوم بتصفية تعديلات ROM والتصحيحات والرسومات والمحتويات الأخرى غير المتعلقة بالألعاب من نتائج البحث."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "البحث القديم",
|
||||
"tooltip": "يعيد توجيه بحثك إلى نتائج التنسيق الجدولي القديم. ستحمل هذه النتائج بشكل أسرع لأنها ستتجاهل سحب البيانات الوصفية."
|
||||
}
|
||||
},
|
||||
"save": "حفظ الإعدادات"
|
||||
@@ -77,6 +103,7 @@
|
||||
"queries": "عدد الاستعلامات:",
|
||||
"files": "الملفات المعروفة:",
|
||||
"terms": "عدد المصطلحات:",
|
||||
"metadata": "الملفات مع البيانات الوصفية:",
|
||||
"last_crawl": "وقت آخر زحف:"
|
||||
},
|
||||
"emulator": {
|
||||
@@ -106,6 +133,7 @@
|
||||
"download": "تنزيل",
|
||||
"play": "تشغيل",
|
||||
"not_available": "----",
|
||||
"not_available_tooltip": "المحاكاة عبر الويب غير متوفرة لهذا العنوان إما لأنه ليس لعبة أو لأن المنصة غير مدعومة.",
|
||||
"disclaimer": "يقوم هذا المحاكي بتحميل الألعاب مباشرة من {{link}}. تعرف على المزيد في صفحة {{about}}."
|
||||
},
|
||||
"results": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "{{seconds}} সেকেন্ডে",
|
||||
"indexing": "ইনডেক্সিং চলছে, তালিকায় কিছু অনুপস্থিত থাকলে কয়েক মিনিট পরে আবার চেষ্টা করুন",
|
||||
"non_game_filter": "নন-গেম কন্টেন্ট ফিল্টার সক্রিয় আছে",
|
||||
"displaying_results": "{{start}} থেকে {{end}} পর্যন্ত ফলাফল দেখানো হচ্ছে।"
|
||||
"displaying_results": "{{start}} থেকে {{end}} পর্যন্ত ফলাফল দেখানো হচ্ছে।",
|
||||
"no_description": "কোন বিবরণ পাওয়া যায়নি।",
|
||||
"no_metadata": "কোন মেটাডেটা পাওয়া যায়নি।",
|
||||
"released": "প্রকাশিত:",
|
||||
"release_date": "প্রকাশের তারিখ:",
|
||||
"region": "অঞ্চল:",
|
||||
"platform": "প্ল্যাটফর্ম:",
|
||||
"genre": "ধরন:",
|
||||
"published": "প্রকাশক:",
|
||||
"developed": "ডেভেলপার:",
|
||||
"modes": "গেমপ্লে মোড:",
|
||||
"download": "ডাউনলোড",
|
||||
"filename": "ফাইলের নাম:",
|
||||
"release_group": "রিলিজ গ্রুপ:",
|
||||
"upload_date": "আপলোডের তারিখ:",
|
||||
"more_info": "আরও তথ্য",
|
||||
"size": "সাইজ:",
|
||||
"old_experience": "পুরানো অনুসন্ধান অভিজ্ঞতা ব্যবহার করা হচ্ছে।",
|
||||
"new_experience": "নতুন অনুসন্ধান অভিজ্ঞতা ব্যবহার করা হচ্ছে।"
|
||||
},
|
||||
"about": {
|
||||
"title": "সম্পর্কে",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "সার্চ ইঞ্জিন তৈরি করেছেন",
|
||||
"view_github": "GitHub-এ প্রকল্প দেখুন"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "মেটাডেটা তথ্য",
|
||||
"description": "এই ওয়েবসাইট {{metadata_source}} থেকে গেমস সম্পর্কে মেটাডেটা তথ্য সংগ্রহ করে। ROM নামের অমিল বা পরিষেবা প্রদানকারীর সমস্যার কারণে কিছু মেটাডেটা অনুপস্থিত বা ভুল হতে পারে।"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -68,16 +90,21 @@
|
||||
},
|
||||
"hide_non_game": {
|
||||
"label": "নন-গেম কন্টেন্ট লুকান",
|
||||
"tooltip": "ROM হ্যাকস, প্যাচ, আর্টওয়ার্ক এবং অন্যান্য নন-গেম কন্টেন্ট অনুসন্ধান ফলাফল থেকে ফিল্টার করে।"
|
||||
"tooltip": "সার্চ রেজাল্ট থেকে ROM হ্যাকস, প্যাচ, আর্টওয়ার্ক এবং অন্যান্য নন-গেম কন্টেন্ট ফিল্টার করে।"
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "পুরানো সার্চ",
|
||||
"tooltip": "আপনার সার্চকে পুরানো টেবিল ফরম্যাটের রেজাল্টে রিডাইরেক্ট করে। এগুলি মেটাডেটা পুল করা এড়িয়ে যাবে তাই দ্রুত লোড হবে।"
|
||||
}
|
||||
},
|
||||
"save": "সেটিংস সংরক্ষণ করুন"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "অনুসন্ধান সংখ্যা:",
|
||||
"files": "পরিচিত ফাইল:",
|
||||
"terms": "শব্দ সংখ্যা:",
|
||||
"last_crawl": "সর্বশেষ আপডেটের সময়:"
|
||||
"queries": "কোয়েরির সংখ্যা:",
|
||||
"files": "জানা ফাইল:",
|
||||
"terms": "শব্দের সংখ্যা:",
|
||||
"metadata": "মেটাডেটা সহ ফাইল:",
|
||||
"last_crawl": "সর্বশেষ ক্রল সময়:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -106,6 +133,7 @@
|
||||
"download": "ডাউনলোড",
|
||||
"play": "প্লে",
|
||||
"not_available": "----",
|
||||
"not_available_tooltip": "এই টাইটেলের জন্য ওয়েব এমুলেশন উপলব্ধ নয় কারণ এটি হয় একটি গেম নয় অথবা প্ল্যাটফর্মটি সমর্থিত নয়।",
|
||||
"disclaimer": "এই এমুলেটর {{link}} থেকে সরাসরি গেম লোড করে। {{about}} পৃষ্ঠায় আরও জানুন।"
|
||||
},
|
||||
"results": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "in {{seconds}} Sekunden",
|
||||
"indexing": "Indexierung läuft noch. Falls etwas in der Liste fehlt, versuchen Sie es in ein paar Minuten erneut",
|
||||
"non_game_filter": "Filter für Nicht-Spielinhalte ist aktiv",
|
||||
"displaying_results": "Ergebnisse {{start}} bis {{end}} werden angezeigt."
|
||||
"displaying_results": "Ergebnisse {{start}} bis {{end}} werden angezeigt.",
|
||||
"no_description": "Keine Beschreibung gefunden.",
|
||||
"no_metadata": "Keine Metadaten gefunden.",
|
||||
"released": "Veröffentlicht:",
|
||||
"release_date": "Erscheinungsdatum:",
|
||||
"region": "Region:",
|
||||
"platform": "Plattform:",
|
||||
"genre": "Genre:",
|
||||
"published": "Veröffentlicht von:",
|
||||
"developed": "Entwickelt von:",
|
||||
"modes": "Spielmodi:",
|
||||
"download": "Herunterladen",
|
||||
"filename": "Dateiname:",
|
||||
"release_group": "Release-Gruppe:",
|
||||
"upload_date": "Upload-Datum:",
|
||||
"more_info": "Mehr Info",
|
||||
"size": "Größe:",
|
||||
"old_experience": "Alte Sucherfahrung wird verwendet.",
|
||||
"new_experience": "Neue Sucherfahrung wird verwendet."
|
||||
},
|
||||
"about": {
|
||||
"title": "Über uns",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Suchmaschine entwickelt von",
|
||||
"view_github": "Projekt auf GitHub ansehen"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Metadaten-Informationen",
|
||||
"description": "Diese Website bezieht Metadaten-Informationen über Spiele von {{metadata_source}}. Einige Metadaten können aufgrund von Abweichungen zwischen ROM-Namen oder Problemen mit dem Dienstanbieter fehlen oder fehlerhaft sein."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -67,17 +89,22 @@
|
||||
"tooltip": "Erfordert, dass jedes Wort der Suchanfrage gefunden wird."
|
||||
},
|
||||
"hide_non_game": {
|
||||
"label": "Nur Spiele anzeigen",
|
||||
"tooltip": "Blendet ROM-Hacks, Patches, Artworks und andere Inhalte aus, die keine eigentlichen Spiele sind."
|
||||
"label": "Nicht-Spiel-Inhalte ausblenden",
|
||||
"tooltip": "Filtert ROM-Hacks, Patches, Artwork und andere Nicht-Spiel-Inhalte aus den Suchergebnissen."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Alte Suche",
|
||||
"tooltip": "Leitet deine Suche zur alten tabellarischen Ergebnisansicht weiter. Diese laden schneller, da keine Metadaten abgerufen werden."
|
||||
}
|
||||
},
|
||||
"save": "Einstellungen speichern"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "Anzahl der Suchanfragen:",
|
||||
"queries": "Anzahl der Abfragen:",
|
||||
"files": "Bekannte Dateien:",
|
||||
"terms": "Indexierte Begriffe:",
|
||||
"last_crawl": "Letztes Update:"
|
||||
"terms": "Anzahl der Begriffe:",
|
||||
"metadata": "Dateien mit Metadaten:",
|
||||
"last_crawl": "Zeit des letzten Durchlaufs:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -106,6 +133,7 @@
|
||||
"download": "Herunterladen",
|
||||
"play": "Spielen",
|
||||
"not_available": "----",
|
||||
"not_available_tooltip": "Web-Emulation ist für diesen Titel nicht verfügbar, da es sich entweder nicht um ein Spiel handelt oder die Plattform nicht unterstützt wird.",
|
||||
"disclaimer": "Dieser Emulator lädt Spiele direkt von {{link}}. Mehr dazu auf der {{about}}-Seite."
|
||||
},
|
||||
"results": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "in {{seconds}} seconds",
|
||||
"indexing": "Indexing in progress, if the list is missing something please try reloading in a few minutes",
|
||||
"non_game_filter": "Non-game content filter is active",
|
||||
"displaying_results": "Displaying results {{start}} through {{end}}."
|
||||
"no_description": "No descrption found.",
|
||||
"no_metadata": "No metadata found.",
|
||||
"displaying_results": "Displaying results {{start}} through {{end}}.",
|
||||
"released": "Released:",
|
||||
"release_date": "Release date:",
|
||||
"region": "Region:",
|
||||
"platform": "Platform:",
|
||||
"genre": "Genre:",
|
||||
"published": "Published by:",
|
||||
"developed": "Developed by:",
|
||||
"modes": "Gameplay modes:",
|
||||
"download": "Download",
|
||||
"filename": "Filename:",
|
||||
"release_group": "Release group:",
|
||||
"upload_date": "Upload date:",
|
||||
"more_info": "More Info",
|
||||
"size": "Size:",
|
||||
"old_experience": "Using the old search experience.",
|
||||
"new_experience": "Using the new search experience."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Search engine created by",
|
||||
"view_github": "View project on GitHub"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Metadata Information",
|
||||
"description": "This website pulls metadata information about games from {{metadata_source}}. Some metadata may be missing or incorrect due to mismatches between ROM name or problems with the service provider."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -69,6 +91,10 @@
|
||||
"hide_non_game": {
|
||||
"label": "Hide Non-Game Content",
|
||||
"tooltip": "Filters out ROM hacks, patches, artwork, and other non-game content from search results."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Old Search",
|
||||
"tooltip": "Controls whether your searches direct to the new or old results experience."
|
||||
}
|
||||
},
|
||||
"save": "Save Settings"
|
||||
@@ -77,6 +103,7 @@
|
||||
"queries": "Number of Queries:",
|
||||
"files": "Known Files:",
|
||||
"terms": "Term Count:",
|
||||
"metadata": "Files with Metadata:",
|
||||
"last_crawl": "Time of Last Crawl:"
|
||||
},
|
||||
"emulator": {
|
||||
@@ -106,6 +133,7 @@
|
||||
"download": "Download",
|
||||
"play": "Play",
|
||||
"not_available": "----",
|
||||
"not_available_tooltip": "Web emulation is unavailable for this title as it's either not a game or the platform is unsupported.",
|
||||
"disclaimer": "This emulator loads games directly from {{link}}. Learn more on the {{about}} page."
|
||||
},
|
||||
"results": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "en {{seconds}} segundos",
|
||||
"indexing": "Indexación en curso, si falta algo en la lista, inténtelo de nuevo en unos minutos",
|
||||
"non_game_filter": "El filtro de contenido que no es juego está activado",
|
||||
"displaying_results": "Mostrando resultados del {{start}} al {{end}}."
|
||||
"displaying_results": "Mostrando resultados del {{start}} al {{end}}.",
|
||||
"no_description": "No se encontró descripción.",
|
||||
"no_metadata": "No se encontraron metadatos.",
|
||||
"released": "Lanzado:",
|
||||
"release_date": "Fecha de lanzamiento:",
|
||||
"region": "Región:",
|
||||
"platform": "Plataforma:",
|
||||
"genre": "Género:",
|
||||
"published": "Publicado por:",
|
||||
"developed": "Desarrollado por:",
|
||||
"modes": "Modos de juego:",
|
||||
"download": "Descargar",
|
||||
"filename": "Nombre del archivo:",
|
||||
"release_group": "Grupo de lanzamiento:",
|
||||
"upload_date": "Fecha de subida:",
|
||||
"more_info": "Más información",
|
||||
"size": "Tamaño:",
|
||||
"old_experience": "Usando la experiencia de búsqueda antigua.",
|
||||
"new_experience": "Usando la nueva experiencia de búsqueda."
|
||||
},
|
||||
"about": {
|
||||
"title": "Acerca de",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Buscador creado por",
|
||||
"view_github": "Ver proyecto en GitHub"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Información de Metadatos",
|
||||
"description": "Este sitio web obtiene información de metadatos sobre juegos de {{metadata_source}}. Algunos metadatos pueden faltar o ser incorrectos debido a discrepancias entre el nombre de la ROM o problemas con el proveedor del servicio."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -67,17 +89,22 @@
|
||||
"tooltip": "Requiere que todas las palabras de la búsqueda tengan coincidencias."
|
||||
},
|
||||
"hide_non_game": {
|
||||
"label": "Ocultar contenido que no son juegos",
|
||||
"tooltip": "Filtra hacks de ROM, parches, imágenes y otros contenidos que no son juegos de los resultados de búsqueda."
|
||||
"label": "Ocultar contenido que no es juego",
|
||||
"tooltip": "Filtra ROM hacks, parches, ilustraciones y otro contenido que no sea juegos de los resultados de búsqueda."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Búsqueda antigua",
|
||||
"tooltip": "Redirige tu búsqueda al antiguo formato de tabla de resultados. Estos cargarán más rápido ya que ignorarán la obtención de metadatos."
|
||||
}
|
||||
},
|
||||
"save": "Guardar Configuración"
|
||||
"save": "Guardar ajustes"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "Número de Consultas:",
|
||||
"files": "Archivos Conocidos:",
|
||||
"terms": "Cantidad de Términos:",
|
||||
"last_crawl": "Última Actualización:"
|
||||
"queries": "Número de consultas:",
|
||||
"files": "Archivos conocidos:",
|
||||
"terms": "Recuento de términos:",
|
||||
"metadata": "Archivos con Metadatos:",
|
||||
"last_crawl": "Hora del último rastreo:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -106,7 +133,8 @@
|
||||
"download": "Descargar",
|
||||
"play": "Jugar",
|
||||
"not_available": "----",
|
||||
"disclaimer": "Este emulador carga juegos directamente desde {{link}}. Más información en la página de {{about}}."
|
||||
"not_available_tooltip": "La emulación web no está disponible para este título ya que no es un juego o la plataforma no es compatible.",
|
||||
"disclaimer": "Este emulador carga juegos directamente desde {{link}}. Más información en la página {{about}}."
|
||||
},
|
||||
"results": {
|
||||
"table": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "en {{seconds}} secondes",
|
||||
"indexing": "Indexation en cours, si quelque chose manque dans la liste, réessayez dans quelques minutes",
|
||||
"non_game_filter": "Le filtre de contenu non-jeu est activé",
|
||||
"displaying_results": "Résultats {{start}} à {{end}}."
|
||||
"displaying_results": "Résultats {{start}} à {{end}}.",
|
||||
"no_description": "Aucune description trouvée.",
|
||||
"no_metadata": "Aucune métadonnée trouvée.",
|
||||
"released": "Sorti le :",
|
||||
"release_date": "Date de sortie :",
|
||||
"region": "Région :",
|
||||
"platform": "Plateforme :",
|
||||
"genre": "Genre :",
|
||||
"published": "Publié par :",
|
||||
"developed": "Développé par :",
|
||||
"modes": "Modes de jeu :",
|
||||
"download": "Télécharger",
|
||||
"filename": "Nom du fichier :",
|
||||
"release_group": "Groupe de release :",
|
||||
"upload_date": "Date de téléchargement :",
|
||||
"more_info": "Plus d'infos",
|
||||
"size": "Taille :",
|
||||
"old_experience": "Utilisation de l'ancienne expérience de recherche.",
|
||||
"new_experience": "Utilisation de la nouvelle expérience de recherche."
|
||||
},
|
||||
"about": {
|
||||
"title": "À propos",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Moteur de recherche créé par",
|
||||
"view_github": "Voir le projet sur GitHub"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Informations sur les Métadonnées",
|
||||
"description": "Ce site web extrait les informations de métadonnées sur les jeux de {{metadata_source}}. Certaines métadonnées peuvent être manquantes ou incorrectes en raison de différences entre le nom de la ROM ou de problèmes avec le fournisseur de service."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -69,6 +91,10 @@
|
||||
"hide_non_game": {
|
||||
"label": "Masquer le contenu non-jeu",
|
||||
"tooltip": "Filtre les ROM hackées, les patches, les illustrations et autres contenus qui ne sont pas des jeux dans les résultats de recherche."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Ancienne recherche",
|
||||
"tooltip": "Contrôle si vos recherches utilisent la nouvelle ou l'ancienne expérience de résultats."
|
||||
}
|
||||
},
|
||||
"save": "Enregistrer les paramètres"
|
||||
@@ -77,7 +103,8 @@
|
||||
"queries": "Nombre de requêtes :",
|
||||
"files": "Fichiers connus :",
|
||||
"terms": "Nombre de termes :",
|
||||
"last_crawl": "Dernière mise à jour :"
|
||||
"metadata": "Fichiers avec Métadonnées :",
|
||||
"last_crawl": "Heure du dernier scan :"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -95,7 +122,8 @@
|
||||
"non_game": "Attention : Ce fichier n'est peut-être pas une ROM de jeu et pourrait ne pas fonctionner correctement dans l'émulateur web.",
|
||||
"see_about": "Consultez la page {{link}} pour plus d'informations.",
|
||||
"no_data": "Aucune donnée d'émulateur disponible.",
|
||||
"https": "Connexion HTTP non sécurisée : Certains émulateurs nécessitent HTTPS pour fonctionner correctement. Ce site n'est pas configuré correctement."
|
||||
"https": "Connexion HTTP non sécurisée : Certains émulateurs nécessitent HTTPS pour fonctionner correctement. Ce site n'est pas configuré correctement.",
|
||||
"not_available_tooltip": "L'émulation web n'est pas disponible pour ce titre car ce n'est pas un jeu ou la plateforme n'est pas prise en charge."
|
||||
},
|
||||
"console": {
|
||||
"about": "Cet émulateur en ligne exécute des jeux directement depuis l'archive publique de Myrient.",
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "{{seconds}} सेकंड में",
|
||||
"indexing": "इंडेक्सिंग प्रगति पर है, अगर सूची में कुछ गायब है तो कृपया कुछ मिनटों में पुनः प्रयास करें",
|
||||
"non_game_filter": "गैर-गेम सामग्री फ़िल्टर सक्रिय है",
|
||||
"displaying_results": "{{start}} से {{end}} तक के परिणाम दिखा रहे हैं।"
|
||||
"displaying_results": "{{start}} से {{end}} तक के परिणाम दिखा रहे हैं।",
|
||||
"no_description": "कोई विवरण नहीं मिला।",
|
||||
"no_metadata": "कोई मेटाडेटा नहीं मिला।",
|
||||
"released": "रिलीज़ हुआ:",
|
||||
"release_date": "रिलीज़ की तारीख:",
|
||||
"region": "क्षेत्र:",
|
||||
"platform": "प्लेटफ़ॉर्म:",
|
||||
"genre": "श्रेणी:",
|
||||
"published": "प्रकाशक:",
|
||||
"developed": "डेवलपर:",
|
||||
"modes": "गेमप्ले मोड:",
|
||||
"download": "डाउनलोड",
|
||||
"filename": "फ़ाइल का नाम:",
|
||||
"release_group": "रिलीज़ ग्रुप:",
|
||||
"upload_date": "अपलोड की तारीख:",
|
||||
"more_info": "अधिक जानकारी",
|
||||
"size": "आकार:",
|
||||
"old_experience": "पुरानी खोज अनुभव का उपयोग किया जा रहा है।",
|
||||
"new_experience": "नई खोज अनुभव का उपयोग किया जा रहा है।"
|
||||
},
|
||||
"about": {
|
||||
"title": "परिचय",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "खोज इंजन किसके द्वारा बनाया गया:",
|
||||
"view_github": "GitHub पर प्रोजेक्ट देखें"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "मेटाडेटा जानकारी",
|
||||
"description": "यह वेबसाइट {{metadata_source}} से गेम्स के बारे में मेटाडेटा जानकारी प्राप्त करती है। ROM नाम में असंगतता या सेवा प्रदाता की समस्याओं के कारण कुछ मेटाडेटा अनुपलब्ध या गलत हो सकता है।"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -69,15 +91,20 @@
|
||||
"hide_non_game": {
|
||||
"label": "गैर-गेम सामग्री छिपाएं",
|
||||
"tooltip": "खोज परिणामों से ROM हैक्स, पैच, आर्टवर्क और अन्य गैर-गेम सामग्री को फ़िल्टर करता है।"
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "पुरानी खोज",
|
||||
"tooltip": "आपकी खोज को पुराने टेबल फॉर्मैट वाले परिणामों में भेजता है। ये मेटाडेटा को नज़रअंदाज़ करेंगे इसलिए तेज़ी से लोड होंगे।"
|
||||
}
|
||||
},
|
||||
"save": "सेटिंग्स सहेजें"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "क्वेरी की संख्या:",
|
||||
"files": "ज्ञात फाइलें:",
|
||||
"files": "ज्ञात फ़ाइलें:",
|
||||
"terms": "शब्दों की संख्या:",
|
||||
"last_crawl": "अंतिम अपडेट का समय:"
|
||||
"metadata": "मेटाडेटा वाली फ़ाइलें:",
|
||||
"last_crawl": "अंतिम क्रॉल का समय:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -106,6 +133,7 @@
|
||||
"download": "डाउनलोड",
|
||||
"play": "खेलें",
|
||||
"not_available": "----",
|
||||
"not_available_tooltip": "इस शीर्षक के लिए वेब एमुलेशन उपलब्ध नहीं है क्योंकि यह या तो एक गेम नहीं है या प्लेटफ़ॉर्म समर्थित नहीं है।",
|
||||
"disclaimer": "यह एमुलेटर {{link}} से सीधे गेम लोड करता है। {{about}} पृष्ठ पर अधिक जानें।"
|
||||
},
|
||||
"results": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "in {{seconds}} secondi",
|
||||
"indexing": "Indicizzazione in corso, se manca qualcosa nell'elenco riprova tra qualche minuto",
|
||||
"non_game_filter": "Filtro per contenuti non-gioco attivo",
|
||||
"displaying_results": "Risultati da {{start}} a {{end}}."
|
||||
"displaying_results": "Risultati da {{start}} a {{end}}.",
|
||||
"no_description": "Nessuna descrizione trovata.",
|
||||
"no_metadata": "Nessun metadato trovato.",
|
||||
"released": "Rilasciato:",
|
||||
"release_date": "Data di uscita:",
|
||||
"region": "Regione:",
|
||||
"platform": "Piattaforma:",
|
||||
"genre": "Genere:",
|
||||
"published": "Pubblicato da:",
|
||||
"developed": "Sviluppato da:",
|
||||
"modes": "Modalità di gioco:",
|
||||
"download": "Scarica",
|
||||
"filename": "Nome file:",
|
||||
"release_group": "Gruppo di release:",
|
||||
"upload_date": "Data di caricamento:",
|
||||
"more_info": "Più informazioni",
|
||||
"size": "Dimensione:",
|
||||
"old_experience": "Utilizzo della vecchia esperienza di ricerca.",
|
||||
"new_experience": "Utilizzo della nuova esperienza di ricerca."
|
||||
},
|
||||
"about": {
|
||||
"title": "Informazioni",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Motore di ricerca creato da",
|
||||
"view_github": "Visualizza progetto su GitHub"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Informazioni sui Metadati",
|
||||
"description": "Questo sito web estrae informazioni sui metadati dei giochi da {{metadata_source}}. Alcuni metadati potrebbero essere mancanti o incorretti a causa di discrepanze tra il nome della ROM o problemi con il fornitore del servizio."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -68,16 +90,21 @@
|
||||
},
|
||||
"hide_non_game": {
|
||||
"label": "Nascondi contenuti non di gioco",
|
||||
"tooltip": "Filtra ROM modificate, patch, grafica e altri contenuti non di gioco dai risultati della ricerca."
|
||||
"tooltip": "Filtra ROM hack, patch, artwork e altri contenuti non di gioco dai risultati di ricerca."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Ricerca vecchia",
|
||||
"tooltip": "Reindirizza la tua ricerca ai risultati nel vecchio formato tabellare. Questi caricheranno più velocemente poiché ignoreranno il recupero dei metadati."
|
||||
}
|
||||
},
|
||||
"save": "Salva Impostazioni"
|
||||
"save": "Salva impostazioni"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "Numero di Ricerche:",
|
||||
"files": "File Conosciuti:",
|
||||
"terms": "Numero di Termini:",
|
||||
"last_crawl": "Ultimo Aggiornamento:"
|
||||
"queries": "Numero di query:",
|
||||
"files": "File conosciuti:",
|
||||
"terms": "Conteggio termini:",
|
||||
"metadata": "File con Metadati:",
|
||||
"last_crawl": "Ora dell'ultima scansione:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -106,6 +133,7 @@
|
||||
"download": "Scarica",
|
||||
"play": "Gioca",
|
||||
"not_available": "----",
|
||||
"not_available_tooltip": "L'emulazione web non è disponibile per questo titolo poiché non è un gioco o la piattaforma non è supportata.",
|
||||
"disclaimer": "Questo emulatore carica i giochi direttamente da {{link}}. Maggiori informazioni nella pagina {{about}}."
|
||||
},
|
||||
"results": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "{{seconds}}秒で検索完了",
|
||||
"indexing": "インデックス作成中です。リストに表示されないものがある場合は、数分後に再度お試しください",
|
||||
"non_game_filter": "非ゲームコンテンツフィルターが有効です",
|
||||
"displaying_results": "{{start}}件目から{{end}}件目を表示中。"
|
||||
"displaying_results": "{{start}}件目から{{end}}件目を表示中。",
|
||||
"no_description": "説明が見つかりません。",
|
||||
"no_metadata": "メタデータが見つかりません。",
|
||||
"released": "発売日:",
|
||||
"release_date": "リリース日:",
|
||||
"region": "地域:",
|
||||
"platform": "プラットフォーム:",
|
||||
"genre": "ジャンル:",
|
||||
"published": "発売元:",
|
||||
"developed": "開発元:",
|
||||
"modes": "ゲームモード:",
|
||||
"download": "ダウンロード",
|
||||
"filename": "ファイル名:",
|
||||
"release_group": "リリースグループ:",
|
||||
"upload_date": "アップロード日:",
|
||||
"more_info": "詳細情報",
|
||||
"size": "サイズ:",
|
||||
"old_experience": "旧検索エクスペリエンスを使用中。",
|
||||
"new_experience": "新検索エクスペリエンスを使用中。"
|
||||
},
|
||||
"about": {
|
||||
"title": "サイトについて",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "検索エンジン開発者:",
|
||||
"view_github": "GitHubでプロジェクトを見る"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "メタデータ情報",
|
||||
"description": "このウェブサイトは {{metadata_source}} からゲームのメタデータ情報を取得しています。ROMの名前の不一致やサービスプロバイダーの問題により、一部のメタデータが欠落しているか、不正確な場合があります。"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -69,15 +91,20 @@
|
||||
"hide_non_game": {
|
||||
"label": "非ゲームコンテンツを非表示",
|
||||
"tooltip": "ROMハック、パッチ、アートワーク、その他のゲーム以外のコンテンツを検索結果から除外します。"
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "旧検索",
|
||||
"tooltip": "検索結果を新しい表示形式と旧表示形式のどちらで表示するかを切り替えます。"
|
||||
}
|
||||
},
|
||||
"save": "設定を保存"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "検索クエリ数:",
|
||||
"files": "登録ファイル数:",
|
||||
"terms": "インデックス語数:",
|
||||
"last_crawl": "最終更新時刻:"
|
||||
"queries": "クエリ数:",
|
||||
"files": "既知のファイル:",
|
||||
"terms": "用語数:",
|
||||
"metadata": "メタデータ付きファイル:",
|
||||
"last_crawl": "最終クロール時刻:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -95,7 +122,8 @@
|
||||
"non_game": "警告:このファイルはゲームROMではない可能性があり、Webエミュレーターで正しく動作しない場合があります。",
|
||||
"see_about": "詳細については{{link}}ページをご覧ください。",
|
||||
"no_data": "エミュレーターデータがありません。",
|
||||
"https": "安全でないHTTP接続:一部のエミュレーターは正常に動作するためにHTTPSが必要です。このサイトは正しく設定されていません。"
|
||||
"https": "安全でないHTTP接続:一部のエミュレーターは正常に動作するためにHTTPSが必要です。このサイトは正しく設定されていません。",
|
||||
"not_available_tooltip": "このタイトルはゲームではないか、プラットフォームが非対応のため、Webエミュレーションを利用できません。"
|
||||
},
|
||||
"console": {
|
||||
"about": "これはMyrientの公開アーカイブから直接ゲームを実行するオンラインエミュレータです。",
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "{{seconds}}초 소요",
|
||||
"indexing": "인덱싱 중입니다. 목록에 누락된 항목이 있다면 잠시 후 다시 시도해 주세요",
|
||||
"non_game_filter": "비게임 콘텐츠 필터가 활성화되었습니다",
|
||||
"displaying_results": "{{start}}번부터 {{end}}번까지의 결과를 표시합니다."
|
||||
"displaying_results": "{{start}}번부터 {{end}}번까지의 결과를 표시합니다.",
|
||||
"no_description": "설명을 찾을 수 없습니다.",
|
||||
"no_metadata": "메타데이터를 찾을 수 없습니다.",
|
||||
"released": "출시일:",
|
||||
"release_date": "발매일:",
|
||||
"region": "지역:",
|
||||
"platform": "플랫폼:",
|
||||
"genre": "장르:",
|
||||
"published": "퍼블리셔:",
|
||||
"developed": "개발사:",
|
||||
"modes": "게임 모드:",
|
||||
"download": "다운로드",
|
||||
"filename": "파일명:",
|
||||
"release_group": "릴리즈 그룹:",
|
||||
"upload_date": "업로드 날짜:",
|
||||
"more_info": "자세히 보기",
|
||||
"size": "크기:",
|
||||
"old_experience": "이전 검색 환경을 사용 중입니다.",
|
||||
"new_experience": "새로운 검색 환경을 사용 중입니다."
|
||||
},
|
||||
"about": {
|
||||
"title": "소개",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "검색 엔진 개발자:",
|
||||
"view_github": "GitHub에서 프로젝트 보기"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "메타데이터 정보",
|
||||
"description": "이 웹사이트는 {{metadata_source}}에서 게임의 메타데이터 정보를 가져옵니다. ROM 이름의 불일치나 서비스 제공자의 문제로 인해 일부 메타데이터가 누락되거나 부정확할 수 있습니다."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -69,15 +91,20 @@
|
||||
"hide_non_game": {
|
||||
"label": "비게임 콘텐츠 숨기기",
|
||||
"tooltip": "ROM 해킹, 패치, 아트워크 등 게임이 아닌 콘텐츠를 검색 결과에서 제외합니다."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "이전 검색",
|
||||
"tooltip": "검색 결과를 새로운 방식과 이전 방식 중 어느 것으로 표시할지 선택합니다."
|
||||
}
|
||||
},
|
||||
"save": "설정 저장"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "검색 횟수:",
|
||||
"files": "등록된 파일:",
|
||||
"terms": "색인된 단어 수:",
|
||||
"last_crawl": "마지막 업데이트:"
|
||||
"queries": "쿼리 수:",
|
||||
"files": "알려진 파일:",
|
||||
"terms": "용어 수:",
|
||||
"metadata": "메타데이터가 있는 파일:",
|
||||
"last_crawl": "마지막 크롤링 시간:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -95,7 +122,8 @@
|
||||
"non_game": "경고: 이 파일은 게임 ROM이 아닐 수 있으며 웹 에뮬레이터에서 제대로 작동하지 않을 수 있습니다.",
|
||||
"see_about": "자세한 내용은 {{link}} 페이지를 참조하세요.",
|
||||
"no_data": "사용 가능한 에뮬레이터 데이터가 없습니다.",
|
||||
"https": "안전하지 않은 HTTP: 일부 에뮬레이터는 제대로 작동하려면 HTTPS가 필요합니다. 이 사이트는 올바르게 설정되지 않았습니다."
|
||||
"https": "안전하지 않은 HTTP: 일부 에뮬레이터는 제대로 작동하려면 HTTPS가 필요합니다. 이 사이트는 올바르게 설정되지 않았습니다.",
|
||||
"not_available_tooltip": "이 타이틀은 게임이 아니거나 지원되지 않는 플랫폼이기 때문에 웹 에뮬레이션을 사용할 수 없습니다."
|
||||
},
|
||||
"console": {
|
||||
"about": "이 온라인 에뮬레이터는 Myrient의 공개 아카이브에서 직접 게임 롬을 실행합니다.",
|
||||
@@ -103,10 +131,10 @@
|
||||
"more_info": "이 서비스에 대한 자세한 정보는 소개 페이지를 확인하세요."
|
||||
},
|
||||
"recommended": "추천 에뮬레이터",
|
||||
"download": "다운로드",
|
||||
"play": "플레이",
|
||||
"not_available": "----",
|
||||
"disclaimer": "이 에뮬레이터는 {{link}}에서 직접 게임을 불러옵니다. {{about}} 페이지에서 더 자세히 알아보세요."
|
||||
"disclaimer": "이 에뮬레이터는 {{link}}에서 직접 게임을 불러옵니다. {{about}} 페이지에서 더 자세히 알아보세요.",
|
||||
"download": "다운로드"
|
||||
},
|
||||
"results": {
|
||||
"table": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "w {{seconds}} sekund",
|
||||
"indexing": "Trwa indeksowanie, jeśli brakuje czegoś na liście, spróbuj ponownie za kilka minut",
|
||||
"non_game_filter": "Filtr treści niebędących grami jest aktywny",
|
||||
"displaying_results": "Wyświetlanie wyników od {{start}} do {{end}}."
|
||||
"displaying_results": "Wyświetlanie wyników od {{start}} do {{end}}.",
|
||||
"no_description": "Nie znaleziono opisu.",
|
||||
"no_metadata": "Nie znaleziono metadanych.",
|
||||
"released": "Wydano:",
|
||||
"release_date": "Data wydania:",
|
||||
"region": "Region:",
|
||||
"platform": "Platforma:",
|
||||
"genre": "Gatunek:",
|
||||
"published": "Wydawca:",
|
||||
"developed": "Deweloper:",
|
||||
"modes": "Tryby gry:",
|
||||
"download": "Pobierz",
|
||||
"filename": "Nazwa pliku:",
|
||||
"release_group": "Grupa wydania:",
|
||||
"upload_date": "Data przesłania:",
|
||||
"more_info": "Więcej informacji",
|
||||
"size": "Rozmiar:",
|
||||
"old_experience": "Używanie starego interfejsu wyszukiwania.",
|
||||
"new_experience": "Używanie nowego interfejsu wyszukiwania."
|
||||
},
|
||||
"about": {
|
||||
"title": "O nas",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Wyszukiwarka stworzona przez",
|
||||
"view_github": "Zobacz projekt na GitHub"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Informacje o Metadanych",
|
||||
"description": "Ta strona pobiera informacje o metadanych gier z {{metadata_source}}. Niektóre metadane mogą być brakujące lub nieprawidłowe z powodu niezgodności między nazwą ROM-u lub problemów z dostawcą usługi."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -67,8 +89,12 @@
|
||||
"tooltip": "Wymaga, aby wszystkie słowa z zapytania zostały znalezione."
|
||||
},
|
||||
"hide_non_game": {
|
||||
"label": "Ukryj treści niebędące grami",
|
||||
"label": "Ukryj zawartość niebędącą grami",
|
||||
"tooltip": "Filtruje hacki ROM-ów, patche, grafiki i inne treści niebędące grami z wyników wyszukiwania."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Stare wyszukiwanie",
|
||||
"tooltip": "Przekierowuje twoje wyszukiwanie do starego formatu tabelarycznego wyników. Te będą ładować się szybciej, ponieważ pomijają pobieranie metadanych."
|
||||
}
|
||||
},
|
||||
"save": "Zapisz ustawienia"
|
||||
@@ -77,7 +103,8 @@
|
||||
"queries": "Liczba zapytań:",
|
||||
"files": "Znane pliki:",
|
||||
"terms": "Liczba terminów:",
|
||||
"last_crawl": "Ostatnia aktualizacja:"
|
||||
"metadata": "Pliki z Metadanymi:",
|
||||
"last_crawl": "Czas ostatniego skanowania:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -103,10 +130,11 @@
|
||||
"more_info": "Więcej informacji o tej usłudze znajdziesz na stronie O nas."
|
||||
},
|
||||
"recommended": "Polecane emulatory",
|
||||
"download": "Pobierz",
|
||||
"play": "Graj",
|
||||
"not_available": "----",
|
||||
"disclaimer": "Ten emulator ładuje gry bezpośrednio z {{link}}. Dowiedz się więcej na stronie {{about}}."
|
||||
"not_available_tooltip": "Emulacja internetowa nie jest dostępna dla tego tytułu, ponieważ nie jest to gra lub platforma nie jest obsługiwana.",
|
||||
"disclaimer": "Ten emulator ładuje gry bezpośrednio z {{link}}. Dowiedz się więcej na stronie {{about}}.",
|
||||
"download": "Pobierz"
|
||||
},
|
||||
"results": {
|
||||
"table": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "em {{seconds}} segundos",
|
||||
"indexing": "Indexação em andamento, se algo estiver faltando na lista, tente novamente em alguns minutos",
|
||||
"non_game_filter": "Filtro de conteúdo não-jogo está ativo",
|
||||
"displaying_results": "Mostrando resultados de {{start}} até {{end}}."
|
||||
"displaying_results": "Mostrando resultados de {{start}} até {{end}}.",
|
||||
"no_description": "Nenhuma descrição encontrada.",
|
||||
"no_metadata": "Nenhum metadado encontrado.",
|
||||
"released": "Lançado:",
|
||||
"release_date": "Data de lançamento:",
|
||||
"region": "Região:",
|
||||
"platform": "Plataforma:",
|
||||
"genre": "Gênero:",
|
||||
"published": "Publicado por:",
|
||||
"developed": "Desenvolvido por:",
|
||||
"modes": "Modos de jogo:",
|
||||
"download": "Baixar",
|
||||
"filename": "Nome do arquivo:",
|
||||
"release_group": "Grupo de lançamento:",
|
||||
"upload_date": "Data de upload:",
|
||||
"more_info": "Mais informações",
|
||||
"size": "Tamanho:",
|
||||
"old_experience": "Usando a experiência de busca antiga.",
|
||||
"new_experience": "Usando a nova experiência de busca."
|
||||
},
|
||||
"about": {
|
||||
"title": "Sobre",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Buscador criado por",
|
||||
"view_github": "Ver projeto no GitHub"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Informações de Metadados",
|
||||
"description": "Este site extrai informações de metadados sobre jogos de {{metadata_source}}. Alguns metadados podem estar ausentes ou incorretos devido a incompatibilidades entre o nome da ROM ou problemas com o provedor do serviço."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -69,15 +91,20 @@
|
||||
"hide_non_game": {
|
||||
"label": "Ocultar Conteúdo Não-Jogo",
|
||||
"tooltip": "Filtra ROMs hackeadas, patches, artes e qualquer outro conteúdo que não seja um jogo dos resultados da busca."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Busca Antiga",
|
||||
"tooltip": "Controla se suas buscas usam a nova ou antiga experiência de resultados."
|
||||
}
|
||||
},
|
||||
"save": "Salvar Configurações"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "Total de buscas:",
|
||||
"queries": "Número de consultas:",
|
||||
"files": "Arquivos conhecidos:",
|
||||
"terms": "Total de termos:",
|
||||
"last_crawl": "Última atualização:"
|
||||
"terms": "Contagem de termos:",
|
||||
"metadata": "Arquivos com Metadados:",
|
||||
"last_crawl": "Hora da última varredura:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -95,7 +122,8 @@
|
||||
"non_game": "Aviso: Este arquivo pode não ser uma ROM de jogo e pode não funcionar corretamente no emulador web.",
|
||||
"see_about": "Veja a página {{link}} para mais informações.",
|
||||
"no_data": "Não há dados de emulador disponíveis.",
|
||||
"https": "Conexão insegura: Alguns emuladores precisam de HTTPS para funcionar corretamente. Esta página não está configurada corretamente."
|
||||
"https": "Conexão insegura: Alguns emuladores precisam de HTTPS para funcionar corretamente. Esta página não está configurada corretamente.",
|
||||
"not_available_tooltip": "A emulação web não está disponível para este título pois não é um jogo ou a plataforma não é suportada."
|
||||
},
|
||||
"console": {
|
||||
"about": "Este é um emulador online que executa jogos diretamente do arquivo público do Myrient.",
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "{{seconds}}byou de kensaku kanryou",
|
||||
"indexing": "Indeksu sakusei chū desu. Risuto ni hyōji sarenai mono ga aru baai wa, sūfun go ni saikenshite kudasai",
|
||||
"non_game_filter": "Hi-gēmu kontentsu fuiruta ga yūkō desu",
|
||||
"displaying_results": "{{start}}ken-me kara {{end}}ken-me wo hyōji chū."
|
||||
"displaying_results": "{{start}}ken-me kara {{end}}ken-me wo hyōji chū.",
|
||||
"no_description": "Setsumei ga mitsukarimasen.",
|
||||
"no_metadata": "Metadēta ga mitsukarimasen.",
|
||||
"released": "Hatsubai:",
|
||||
"release_date": "Hatsubai-bi:",
|
||||
"region": "Chiiki:",
|
||||
"platform": "Purattofōmu:",
|
||||
"genre": "Janru:",
|
||||
"published": "Hatsubaisha:",
|
||||
"developed": "Kaihatsusha:",
|
||||
"modes": "Gēmu mōdo:",
|
||||
"download": "Daunrōdo",
|
||||
"filename": "Fairu mei:",
|
||||
"release_group": "Rirīsu gurūpu:",
|
||||
"upload_date": "Apurōdo-bi:",
|
||||
"more_info": "Kuwashii jōhō",
|
||||
"size": "Saizu:",
|
||||
"old_experience": "Kyū kensaku taiken wo shiyō chū desu.",
|
||||
"new_experience": "Atarashii kensaku taiken wo shiyō chū desu."
|
||||
},
|
||||
"about": {
|
||||
"title": "Saito ni tsuite",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Kensaku enjin kaihatsusha:",
|
||||
"view_github": "GitHub de purojekuto wo miru"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Metadēta Jōhō",
|
||||
"description": "Kono websaito wa {{metadata_source}} kara gēmu no metadēta jōhō wo shutoku shiteimasu. ROM no namae no fuicchi ya sābisu purobaida no mondai ni yori, ichibu no metadēta ga ketsujo shiteiru ka, fuseikaku na baai ga arimasu."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -69,15 +91,20 @@
|
||||
"hide_non_game": {
|
||||
"label": "Hi-gēmu Kontentsu wo Hyōji Shinai",
|
||||
"tooltip": "ROM hakku, patchi, ātowāku, sono ta no gēmu igai no kontentsu wo kensaku kekka kara jokyo shimasu."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Kyū Kensaku",
|
||||
"tooltip": "Kensaku kekka wo kyū tēburu keishiki ni redirekuto shimasu. Kore wa metadēta no shutoku wo sukippu suru tame, yori hayaku rōdo saremasu."
|
||||
}
|
||||
},
|
||||
"save": "Settei wo Hozon"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "Kensaku Kueri Sū:",
|
||||
"files": "Tōroku Fairu Sū:",
|
||||
"terms": "Indeksu Tango Sū:",
|
||||
"last_crawl": "Saishū Kōshin Jikoku:"
|
||||
"queries": "Kueri-suu:",
|
||||
"files": "Shiru fairu:",
|
||||
"terms": "Yougo-suu:",
|
||||
"metadata": "Metadata-tsuki Fairu:",
|
||||
"last_crawl": "Saigo no kuroru jikoku:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -103,10 +130,11 @@
|
||||
"more_info": "Kono sābisu ni tsuite no kuwashii jōhō wa, saito ni tsuite no pēji wo goran kudasai."
|
||||
},
|
||||
"recommended": "Suishou Emyurēta",
|
||||
"download": "Daunrōdo",
|
||||
"play": "Purei",
|
||||
"not_available": "----",
|
||||
"disclaimer": "Kono emyurēta wa {{link}} kara chokusetsu gēmu wo yomikomi masu. Kuwashiku wa {{about}} pēji wo goran kudasai."
|
||||
"disclaimer": "Kono emyurēta wa {{link}} kara chokusetsu gēmu wo yomikomi masu. Kuwashiku wa {{about}} pēji wo goran kudasai.",
|
||||
"download": "Daunrōdo",
|
||||
"not_available_tooltip": "Kono taitoru wa gēmu de wa nai ka, purattofōmu ga sapōto sarete inai tame, webu emyurēshon wa riyō dekimasen."
|
||||
},
|
||||
"results": {
|
||||
"table": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "за {{seconds}} секунд",
|
||||
"indexing": "Идёт индексация, если в списке чего-то не хватает, попробуйте перезагрузить через несколько минут",
|
||||
"non_game_filter": "Активирован фильтр не-игрового контента",
|
||||
"displaying_results": "Показаны результаты с {{start}} по {{end}}."
|
||||
"displaying_results": "Показаны результаты с {{start}} по {{end}}.",
|
||||
"no_description": "Описание не найдено.",
|
||||
"no_metadata": "Метаданные не найдены.",
|
||||
"released": "Выпущено:",
|
||||
"release_date": "Дата выхода:",
|
||||
"region": "Регион:",
|
||||
"platform": "Платформа:",
|
||||
"genre": "Жанр:",
|
||||
"published": "Издатель:",
|
||||
"developed": "Разработчик:",
|
||||
"modes": "Режимы игры:",
|
||||
"download": "Скачать",
|
||||
"filename": "Имя файла:",
|
||||
"release_group": "Релиз-группа:",
|
||||
"upload_date": "Дата загрузки:",
|
||||
"more_info": "Подробнее",
|
||||
"size": "Размер:",
|
||||
"old_experience": "Используется старый интерфейс поиска.",
|
||||
"new_experience": "Используется новый интерфейс поиска."
|
||||
},
|
||||
"about": {
|
||||
"title": "О сайте",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Поисковая система создана",
|
||||
"view_github": "Посмотреть проект на GitHub"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Информация о Метаданных",
|
||||
"description": "Этот веб-сайт получает метаданные об играх из {{metadata_source}}. Некоторые метаданные могут отсутствовать или быть неточными из-за несоответствий в названиях ROM или проблем с поставщиком услуг."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -68,7 +90,11 @@
|
||||
},
|
||||
"hide_non_game": {
|
||||
"label": "Скрыть не-игровой контент",
|
||||
"tooltip": "Отфильтровывает ROM-хаки, патчи, иллюстрации и другой не-игровой контент из результатов поиска."
|
||||
"tooltip": "Фильтрует ROM-хаки, патчи, изображения и другой не-игровой контент из результатов поиска."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Старый поиск",
|
||||
"tooltip": "Перенаправляет ваш поиск на старый табличный формат результатов. Они загрузятся быстрее, так как не будут получать метаданные."
|
||||
}
|
||||
},
|
||||
"save": "Сохранить настройки"
|
||||
@@ -77,7 +103,8 @@
|
||||
"queries": "Количество запросов:",
|
||||
"files": "Известные файлы:",
|
||||
"terms": "Количество терминов:",
|
||||
"last_crawl": "Время последнего обхода:"
|
||||
"metadata": "Файлы с Метаданными:",
|
||||
"last_crawl": "Время последнего сканирования:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -103,9 +130,9 @@
|
||||
"more_info": "Для получения дополнительной информации об этом сервисе, пожалуйста, посетите страницу О сайте."
|
||||
},
|
||||
"recommended": "Рекомендуемые эмуляторы",
|
||||
"download": "Скачать",
|
||||
"play": "Играть",
|
||||
"not_available": "----",
|
||||
"not_available_tooltip": "Веб-эмуляция недоступна для этого заголовка, так как это либо не игра, либо платформа не поддерживается.",
|
||||
"disclaimer": "Этот эмулятор загружает игры напрямую из {{link}}. Узнайте больше на странице {{about}}."
|
||||
},
|
||||
"results": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "{{seconds}} saniyede",
|
||||
"indexing": "İndeksleme devam ediyor, listede eksik bir şey varsa lütfen birkaç dakika sonra tekrar deneyin",
|
||||
"non_game_filter": "Oyun dışı içerik filtresi etkin",
|
||||
"displaying_results": "{{start}} ile {{end}} arası sonuçlar gösteriliyor."
|
||||
"displaying_results": "{{start}} ile {{end}} arası sonuçlar gösteriliyor.",
|
||||
"no_description": "Açıklama bulunamadı.",
|
||||
"no_metadata": "Meta veri bulunamadı.",
|
||||
"released": "Yayınlandı:",
|
||||
"release_date": "Çıkış tarihi:",
|
||||
"region": "Bölge:",
|
||||
"platform": "Platform:",
|
||||
"genre": "Tür:",
|
||||
"published": "Yayıncı:",
|
||||
"developed": "Geliştirici:",
|
||||
"modes": "Oyun modları:",
|
||||
"download": "İndir",
|
||||
"filename": "Dosya adı:",
|
||||
"release_group": "Yayın grubu:",
|
||||
"upload_date": "Yükleme tarihi:",
|
||||
"more_info": "Daha fazla bilgi",
|
||||
"size": "Boyut:",
|
||||
"old_experience": "Eski arama deneyimi kullanılıyor.",
|
||||
"new_experience": "Yeni arama deneyimi kullanılıyor."
|
||||
},
|
||||
"about": {
|
||||
"title": "Hakkında",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "Arama motoru şu kişi tarafından oluşturuldu:",
|
||||
"view_github": "Projeyi GitHub'da görüntüle"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "Meta Veri Bilgileri",
|
||||
"description": "Bu web sitesi, oyunlar hakkındaki meta veri bilgilerini {{metadata_source}} kaynağından çeker. ROM adı uyuşmazlıkları veya servis sağlayıcı sorunları nedeniyle bazı meta veriler eksik veya hatalı olabilir."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -67,17 +89,22 @@
|
||||
"tooltip": "Arama sorgusundaki tüm kelimelerin eşleşmesini gerektirir."
|
||||
},
|
||||
"hide_non_game": {
|
||||
"label": "Oyun Olmayan İçeriği Gizle",
|
||||
"tooltip": "ROM hackleri, yamalar, görseller ve diğer oyun dışı içerikleri arama sonuçlarından filtreler."
|
||||
"label": "Oyun Dışı İçeriği Gizle",
|
||||
"tooltip": "ROM hack'leri, yamalar, görseller ve diğer oyun dışı içerikleri arama sonuçlarından filtreler."
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "Eski Arama",
|
||||
"tooltip": "Aramanızı eski tablo formatındaki sonuçlara yönlendirir. Bunlar meta verileri çekmeyeceği için daha hızlı yüklenecektir."
|
||||
}
|
||||
},
|
||||
"save": "Ayarları Kaydet"
|
||||
},
|
||||
"footer": {
|
||||
"queries": "Sorgu Sayısı:",
|
||||
"files": "Bilinen Dosyalar:",
|
||||
"terms": "Terim Sayısı:",
|
||||
"last_crawl": "Son Güncellenme Zamanı:"
|
||||
"queries": "Sorgu sayısı:",
|
||||
"files": "Bilinen dosyalar:",
|
||||
"terms": "Terim sayısı:",
|
||||
"metadata": "Meta Verili Dosyalar:",
|
||||
"last_crawl": "Son tarama zamanı:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -106,6 +133,7 @@
|
||||
"download": "İndir",
|
||||
"play": "Oyna",
|
||||
"not_available": "----",
|
||||
"not_available_tooltip": "Bu başlık için web emülasyonu kullanılamıyor çünkü ya bir oyun değil ya da platform desteklenmiyor.",
|
||||
"disclaimer": "Bu emülatör oyunları doğrudan {{link}} adresinden yükler. {{about}} sayfasında daha fazla bilgi edinin."
|
||||
},
|
||||
"results": {
|
||||
|
||||
@@ -21,7 +21,25 @@
|
||||
"in_seconds": "用时{{seconds}}秒",
|
||||
"indexing": "索引正在进行中,如果列表中缺少内容,请稍后再试",
|
||||
"non_game_filter": "非游戏内容过滤已启用",
|
||||
"displaying_results": "显示第{{start}}至{{end}}条结果。"
|
||||
"displaying_results": "显示第{{start}}至{{end}}条结果。",
|
||||
"no_description": "未找到描述。",
|
||||
"no_metadata": "未找到元数据。",
|
||||
"released": "发布时间:",
|
||||
"release_date": "发行日期:",
|
||||
"region": "地区:",
|
||||
"platform": "平台:",
|
||||
"genre": "类型:",
|
||||
"published": "发行商:",
|
||||
"developed": "开发商:",
|
||||
"modes": "游戏模式:",
|
||||
"download": "下载",
|
||||
"filename": "文件名:",
|
||||
"release_group": "发布组:",
|
||||
"upload_date": "上传日期:",
|
||||
"more_info": "更多信息",
|
||||
"size": "大小:",
|
||||
"old_experience": "正在使用旧版搜索体验。",
|
||||
"new_experience": "正在使用新版搜索体验。"
|
||||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
@@ -40,6 +58,10 @@
|
||||
"credits": {
|
||||
"created_by": "搜索引擎开发者:",
|
||||
"view_github": "在GitHub上查看项目"
|
||||
},
|
||||
"metadata": {
|
||||
"title": "元数据信息",
|
||||
"description": "本网站从 {{metadata_source}} 获取游戏的元数据信息。由于 ROM 名称不匹配或服务提供商的问题,某些元数据可能缺失或不准确。"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -69,6 +91,10 @@
|
||||
"hide_non_game": {
|
||||
"label": "隐藏非游戏内容",
|
||||
"tooltip": "从搜索结果中过滤掉ROM修改、补丁、游戏插图等非游戏内容。"
|
||||
},
|
||||
"use_old_results": {
|
||||
"label": "旧版搜索",
|
||||
"tooltip": "控制您的搜索是使用新版还是旧版结果体验。"
|
||||
}
|
||||
},
|
||||
"save": "保存设置"
|
||||
@@ -76,8 +102,9 @@
|
||||
"footer": {
|
||||
"queries": "查询次数:",
|
||||
"files": "已知文件:",
|
||||
"terms": "索引词数:",
|
||||
"last_crawl": "最近更新时间:"
|
||||
"terms": "术语数量:",
|
||||
"metadata": "带元数据的文件:",
|
||||
"last_crawl": "最后爬取时间:"
|
||||
},
|
||||
"emulator": {
|
||||
"loading": {
|
||||
@@ -95,7 +122,8 @@
|
||||
"non_game": "警告:此文件可能不是游戏ROM,可能无法在网页模拟器中正常运行。",
|
||||
"see_about": "更多信息请参阅{{link}}页面。",
|
||||
"no_data": "没有可用的模拟器数据。",
|
||||
"https": "不安全的HTTP连接:部分模拟器需要HTTPS才能正常运行。当前站点配置不正确。"
|
||||
"https": "不安全的HTTP连接:部分模拟器需要HTTPS才能正常运行。当前站点配置不正确。",
|
||||
"not_available_tooltip": "此标题无法使用网页模拟器,因为它不是游戏或平台不受支持。"
|
||||
},
|
||||
"console": {
|
||||
"about": "这是一个在线模拟器,直接从Myrient的公共档案运行游戏ROM。",
|
||||
|
||||
@@ -8,6 +8,7 @@ services:
|
||||
- BIND_ADDRESS=0.0.0.0
|
||||
- FORCE_FILE_REBUILD=0
|
||||
- DEBUG=0
|
||||
- HOSTNAME=myrient.mahou.one
|
||||
- NODE_ENV=production
|
||||
- MAX_JOB_QUEUE=1000
|
||||
- MAX_FETCH_JOBS=1000
|
||||
@@ -19,6 +20,8 @@ services:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=this-is-a-secure-db-password
|
||||
- ELASTICSEARCH_URL=http://elasticsearch:9200
|
||||
- TWITCH_CLIENT_ID=
|
||||
- TWITCH_CLIENT_SECRET=
|
||||
volumes:
|
||||
- ./data:/usr/src/app/data
|
||||
depends_on:
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { getTableRows, parseOutFile } from "./fileworker.js";
|
||||
import { getTableRows, parseOutFile } from "./workers/fileworker.js";
|
||||
import { Piscina, FixedQueue } from "piscina";
|
||||
import { resolve } from "path";
|
||||
import debugPrint from "./debugprint.js";
|
||||
import { File } from './models/index.js';
|
||||
import { bulkIndexFiles } from './services/elasticsearch.js';
|
||||
import { optimizeDatabaseKws } from "./dboptimize.js";
|
||||
import { timer } from "./time.js";
|
||||
import debugPrint from "../utility/printutils.js";
|
||||
import { File } from "../database/models/index.js";
|
||||
import { bulkIndexFiles } from "../services/elasticsearch.js";
|
||||
import { Timer } from "../utility/time.js";
|
||||
|
||||
let piscina = new Piscina({
|
||||
filename: resolve("./lib", "fileworker.js"),
|
||||
filename: resolve("./lib/crawler/workers/", "fileworker.js"),
|
||||
taskQueue: new FixedQueue(),
|
||||
});
|
||||
|
||||
const BATCH_SIZE = 1000; // Process files in batches for better performance
|
||||
|
||||
export default async function getAllFiles(catList) {
|
||||
var proctime = new timer()
|
||||
var proctime = new Timer();
|
||||
const url = "https://myrient.erista.me/files/";
|
||||
let parentRows = await getTableRows({ url: url, base: "" });
|
||||
let parents = [];
|
||||
@@ -94,7 +93,9 @@ export default async function getAllFiles(catList) {
|
||||
}
|
||||
|
||||
fetchTasks = [];
|
||||
dirStatus = `Directories Remaining: ${dirs.length}, Files Found: ${fileCount}`;
|
||||
dirStatus = `Directories Remaining: ${
|
||||
dirs.length
|
||||
}, Files Found: ${fileCount} (${proctime.elapsed()})`;
|
||||
}
|
||||
|
||||
if (dirs.length == 0 && parseTasks.length > 0) {
|
||||
@@ -122,7 +123,9 @@ export default async function getAllFiles(catList) {
|
||||
}
|
||||
|
||||
parseTasks = [];
|
||||
dirStatus = `Directories Remaining: ${dirs.length}, Files Found: ${fileCount}`;
|
||||
dirStatus = `Directories Remaining: ${
|
||||
dirs.length
|
||||
}, Files Found: ${fileCount} (${proctime.elapsed()})`;
|
||||
}
|
||||
|
||||
if (dirStatus) {
|
||||
@@ -142,7 +145,6 @@ export default async function getAllFiles(catList) {
|
||||
|
||||
console.log(`\nFinished crawling Myrient in ${proctime.elapsed()}.`);
|
||||
await piscina.close();
|
||||
await optimizeDatabaseKws();
|
||||
return fileCount;
|
||||
}
|
||||
|
||||
@@ -153,7 +155,7 @@ async function processBatch(files) {
|
||||
for (let i = 0; i < files.length; i += chunkSize) {
|
||||
const chunk = files.slice(i, i + chunkSize);
|
||||
const dbFiles = await File.bulkCreate(
|
||||
chunk.map(file => ({
|
||||
chunk.map((file) => ({
|
||||
filename: file.filename,
|
||||
path: file.path,
|
||||
size: file.size,
|
||||
@@ -161,20 +163,25 @@ async function processBatch(files) {
|
||||
type: file.type,
|
||||
date: file.date,
|
||||
region: file.region,
|
||||
group: file.group
|
||||
group: file.group,
|
||||
nongame: file.nongame,
|
||||
})),
|
||||
{
|
||||
returning: true,
|
||||
updateOnDuplicate: ['path']
|
||||
updateOnDuplicate: ["path"],
|
||||
}
|
||||
);
|
||||
|
||||
// Index chunk in Elasticsearch
|
||||
await bulkIndexFiles(dbFiles);
|
||||
debugPrint(`Processed ${i + chunk.length} of ${files.length} files in current batch`);
|
||||
debugPrint(
|
||||
`Processed ${i + chunk.length} of ${
|
||||
files.length
|
||||
} files in current batch`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing batch:', error);
|
||||
console.error("Error processing batch:", error);
|
||||
}
|
||||
}
|
||||
|
||||
325
lib/crawler/metadatamanager.js
Normal file
@@ -0,0 +1,325 @@
|
||||
import {
|
||||
twitchAccessToken,
|
||||
igdb,
|
||||
request,
|
||||
multi,
|
||||
} from "@phalcode/ts-igdb-client";
|
||||
import {
|
||||
fields,
|
||||
or,
|
||||
and,
|
||||
where,
|
||||
whereIn,
|
||||
WhereFlags,
|
||||
WhereInFlags,
|
||||
sort,
|
||||
limit,
|
||||
offset,
|
||||
} from "@phalcode/ts-igdb-client";
|
||||
import { File, Metadata } from "../database/database.js";
|
||||
import TaskQueue from "../utility/taskqueue.js";
|
||||
import { singleLineStatus } from "../utility/printutils.js";
|
||||
import { Timer } from "../utility/time.js";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
export default class MetadataManager {
|
||||
constructor() {
|
||||
this.twitchSecrets = {
|
||||
client_id: process.env.TWITCH_CLIENT_ID,
|
||||
client_secret: process.env.TWITCH_CLIENT_SECRET,
|
||||
};
|
||||
this.setupClient();
|
||||
this.queue = new TaskQueue();
|
||||
}
|
||||
gameFields = [
|
||||
"name",
|
||||
"alternative_names.comment",
|
||||
"alternative_names.name",
|
||||
"cover.image_id",
|
||||
"total_rating",
|
||||
"first_release_date",
|
||||
"summary",
|
||||
"genres.name",
|
||||
"involved_companies.company.name",
|
||||
"involved_companies.developer",
|
||||
"involved_companies.publisher",
|
||||
"involved_companies.supporting",
|
||||
"game_modes.name",
|
||||
"game_localizations.name",
|
||||
"game_localizations.region",
|
||||
"game_localizations.region.name",
|
||||
"game_localizations.cover.image_id",
|
||||
"platforms.name",
|
||||
"game_type.type",
|
||||
"screenshots.image_id",
|
||||
"videos.video_id",
|
||||
];
|
||||
|
||||
async setupClient() {
|
||||
try {
|
||||
if (this.twitchSecrets.client_id && this.twitchSecrets.client_secret) {
|
||||
this.accessToken = await twitchAccessToken(this.twitchSecrets);
|
||||
this.client = igdb(this.twitchSecrets.client_id, this.accessToken);
|
||||
const mapFilePath = "./lib/json/maps/igdb_platform.json";
|
||||
this.platformMap = JSON.parse(readFileSync(mapFilePath, "utf8"));
|
||||
if (this.accessToken) {
|
||||
this.authorized = true;
|
||||
this.ready = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.ready = true;
|
||||
this.authorized = false; //disable
|
||||
} catch (error) {
|
||||
this.ready = true;
|
||||
this.authorized = false;
|
||||
}
|
||||
}
|
||||
|
||||
normalizeName(filename) {
|
||||
if (!filename) return;
|
||||
return filename
|
||||
.replace(
|
||||
/\.[A-z]{3,3}|\.|&|-|\+|,|v[0-9]+\.[0-9]+|\[.*?\]|\(.*?\)|the|usa/gi,
|
||||
""
|
||||
)
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
async getIGDBGamesCount(retrying = false) {
|
||||
try {
|
||||
// hack to ensure the client is ready before we do anything
|
||||
while (!this.ready) {
|
||||
await this.sleep(500);
|
||||
}
|
||||
if (this.authorized === false) return 0;
|
||||
const { data } = await this.client
|
||||
.request("games/count")
|
||||
.pipe(
|
||||
and(
|
||||
where("game_type.type", "!=", "Mod"),
|
||||
where("game_type.type", "!=", "DLC")
|
||||
)
|
||||
)
|
||||
.execute();
|
||||
return data.count;
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_BAD_REQUEST" && !retrying) {
|
||||
this.setupClient();
|
||||
return this.getIGDBGamesCount(true);
|
||||
}
|
||||
console.error("Error getting IGDB games count:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async matchAllMetadata(fuzzy = false) {
|
||||
let games = await File.findAll({
|
||||
where: {
|
||||
nongame: false,
|
||||
detailsId: null,
|
||||
},
|
||||
attributes: ["id", "filename"],
|
||||
order: ["id", "filename"],
|
||||
include: { model: Metadata, as: "details" },
|
||||
});
|
||||
let count = games.length;
|
||||
let timer = new Timer();
|
||||
let found = 0;
|
||||
console.log(`Matching ${count} games to metadata.`);
|
||||
for (let x = 0; x < count; x++) {
|
||||
singleLineStatus(
|
||||
`Matching metadata: ${x} / ${count} ${(
|
||||
((x) / count) *
|
||||
100
|
||||
).toFixed(2)}% (${timer.elapsed()}) Total Matches: ${found}`
|
||||
);
|
||||
let game = games[x];
|
||||
let metadata = await Metadata.searchByText(
|
||||
"title",
|
||||
this.normalizeName(game.filename),
|
||||
game.category
|
||||
);
|
||||
if (metadata?.length == 0) {
|
||||
// repeat the search under one of the alternate titles
|
||||
metadata = await Metadata.searchByText(
|
||||
"alternatetitles",
|
||||
this.normalizeName(game.filename),
|
||||
game.category
|
||||
);
|
||||
}
|
||||
if (metadata?.length >= 1) {
|
||||
let md = await Metadata.findByPk(metadata[0].id);
|
||||
await game.setDetails(md);
|
||||
await md.addFile(game);
|
||||
await game.save();
|
||||
await md.save();
|
||||
found++;
|
||||
} else if (fuzzy) {
|
||||
//this is much slower and should only be used if the faster full text search can't find it.
|
||||
let metadata = await Metadata.fuzzySearchByText(
|
||||
"title",
|
||||
this.normalizeName(game.filename),
|
||||
0.8,
|
||||
game.category
|
||||
);
|
||||
if (!metadata?.length == 0) {
|
||||
metadata = await Metadata.fuzzySearchByText(
|
||||
"alternatetitles",
|
||||
this.normalizeName(game.filename),
|
||||
0.8,
|
||||
game.category
|
||||
);
|
||||
}
|
||||
if (metadata?.length >= 1) {
|
||||
let md = await Metadata.findByPk(metadata[0].id);
|
||||
await game.setDetails(md);
|
||||
await md.addFile(game);
|
||||
await game.save();
|
||||
await md.save();
|
||||
found++;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`\nFinished matching metadata to files in ${timer.elapsed()}`);
|
||||
}
|
||||
|
||||
async syncAllMetadata(retrying = false) {
|
||||
try {
|
||||
const timer = new Timer();
|
||||
// hack to ensure the client is ready before we do anything
|
||||
while (!this.ready) {
|
||||
await this.sleep(500);
|
||||
}
|
||||
if (!this.authorized) {
|
||||
console.log(
|
||||
"Twitch credentials are unavailable or invalid; metadata sync is unavailable."
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log("Syncing all metadata...");
|
||||
let count = await this.getIGDBGamesCount();
|
||||
let pageSize = 500;
|
||||
let pages = Math.ceil(count / pageSize);
|
||||
let retryCount = 0;
|
||||
for (let x = 0; x < pages; x++) {
|
||||
if (retryCount == 5) continue;
|
||||
singleLineStatus(
|
||||
`Syncing metadata: ${x * 500} / ${count} ${(
|
||||
((x * 500) / count) *
|
||||
100
|
||||
).toFixed(2)}% (${timer.elapsed()})`
|
||||
);
|
||||
try {
|
||||
let { data } = await this.client
|
||||
.request("games")
|
||||
.pipe(
|
||||
limit(pageSize),
|
||||
offset(x * pageSize),
|
||||
fields(this.gameFields),
|
||||
sort("id")
|
||||
)
|
||||
.execute();
|
||||
for (let y in data) {
|
||||
await this.addMetadataToDb(data[y]);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_BAD_RESPONSE") {
|
||||
x--;
|
||||
await this.sleep(1000);
|
||||
retryCount++;
|
||||
console.log(
|
||||
`Retrieving metadata at offset ${
|
||||
x * 500
|
||||
} failed. Retry count: ${retryCount}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
throw error; //hoist it up
|
||||
}
|
||||
retryCount = 0;
|
||||
}
|
||||
console.log(`\nFinished syncing metadata in ${timer.elapsed()}`);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_BAD_REQUEST" && !retrying) {
|
||||
this.setupClient();
|
||||
return this.syncAllMetadata(true);
|
||||
}
|
||||
console.error("Error syncing all metadata:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async addMetadataToDb(metadata) {
|
||||
try {
|
||||
let md = await Metadata.findByPk(metadata.id);
|
||||
if (!md) {
|
||||
md = await Metadata.build(
|
||||
{
|
||||
id: metadata.id,
|
||||
},
|
||||
{
|
||||
include: File,
|
||||
}
|
||||
);
|
||||
}
|
||||
// I hate this
|
||||
let coverArt = {
|
||||
default: metadata.cover?.image_id,
|
||||
};
|
||||
for (let x in metadata.game_localizations) {
|
||||
let gl = metadata.game_localizations[x];
|
||||
if (gl.region && gl.cover) {
|
||||
coverArt[gl.region.name] = gl.cover.image_id;
|
||||
}
|
||||
}
|
||||
md.title = metadata.name;
|
||||
|
||||
md.description = metadata.summary;
|
||||
md.rating = metadata.total_rating;
|
||||
md.coverartid = JSON.stringify(coverArt);
|
||||
md.releasedate = metadata.first_release_date
|
||||
? new Date(metadata.first_release_date * 1000)
|
||||
: null;
|
||||
md.genre = metadata.genres?.map((genre) => genre.name);
|
||||
md.gamemodes = metadata.game_modes?.map((gm) => gm.name);
|
||||
md.platforms = metadata.platforms?.map(
|
||||
(platform) => this.platformMap[platform.name] || platform.name
|
||||
);
|
||||
md.screenshots = metadata.screenshots?.map((ss) => ss.image_id);
|
||||
md.videos = metadata.videos?.map((v) => v.video_id);
|
||||
md.developers = metadata.involved_companies
|
||||
?.filter((ic) => ic.developer)
|
||||
?.map((ic) => ic.company.name);
|
||||
md.publishers = metadata.involved_companies
|
||||
?.filter((ic) => ic.publisher)
|
||||
?.map((ic) => ic.company.name);
|
||||
//I hate this too
|
||||
let alternates = new Object();
|
||||
if (metadata.alternative_names) {
|
||||
for (let x in metadata.alternative_names) {
|
||||
let an = metadata.alternative_names[x];
|
||||
if (an.comment && an.name) {
|
||||
alternates[an.comment] = an.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (metadata.game_localizations) {
|
||||
for (let x in metadata.game_localizations) {
|
||||
let gl = metadata.game_localizations[x];
|
||||
if (gl.region.name && gl.name) {
|
||||
alternates[gl.region.name] = gl.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
//this needs to remain json as we want the keys to be retained
|
||||
md.alternatetitles = alternates ? JSON.stringify(alternates) : null;
|
||||
await md.save();
|
||||
} catch (error) {
|
||||
console.error("Error adding metadata:", error);
|
||||
}
|
||||
}
|
||||
async sleep(delay) {
|
||||
return new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import innertext from "innertext";
|
||||
import HTMLParse from "node-html-parser";
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, resolve } from "path";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
export async function getTableRows(data) {
|
||||
let retryLeft = 5;
|
||||
@@ -55,7 +58,8 @@ export async function parseOutFile(data) {
|
||||
type: findType(fullName, data.catList),
|
||||
date: innertext(file.querySelector(".date").innerHTML).trim(),
|
||||
region: findRegion(fullName, data.catList),
|
||||
group: findGroup(fullName)
|
||||
group: findGroup(fullName),
|
||||
nongame: checkNonGame(name)
|
||||
};
|
||||
return processedFile;
|
||||
}
|
||||
@@ -166,6 +170,29 @@ function findGroup(str){
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for nonGameTerms
|
||||
let nonGameTermsCache = null;
|
||||
|
||||
function getNonGameTerms() {
|
||||
if (nonGameTermsCache) {
|
||||
return nonGameTermsCache;
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const nonGameTermsPath = resolve(__dirname, '../../json/terms/nonGameTerms.json');
|
||||
nonGameTermsCache = JSON.parse(readFileSync(nonGameTermsPath, 'utf8'));
|
||||
|
||||
return nonGameTermsCache;
|
||||
}
|
||||
|
||||
function checkNonGame(str){
|
||||
const nonGameTerms = getNonGameTerms();
|
||||
const termPatterns = nonGameTerms.terms.map(term => new RegExp(term, 'i'));
|
||||
return termPatterns.some(pattern => pattern.test(str));
|
||||
}
|
||||
|
||||
class HTTPResponseError extends Error {
|
||||
constructor(response) {
|
||||
super(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Sequelize } from 'sequelize';
|
||||
import 'dotenv/config';
|
||||
|
||||
// Import models
|
||||
import defineFile from './models/file.js';
|
||||
import defineQueryCount from './models/queryCount.js';
|
||||
|
||||
const sequelize = new Sequelize(process.env.POSTGRES_DB, process.env.POSTGRES_USER, process.env.POSTGRES_PASSWORD, {
|
||||
host: process.env.POSTGRES_HOST || 'localhost',
|
||||
port: process.env.POSTGRES_PORT || 5432,
|
||||
dialect: 'postgres',
|
||||
logging: process.env.DEBUG === '1' ? console.log : false
|
||||
});
|
||||
|
||||
// Initialize models
|
||||
export const File = defineFile(sequelize);
|
||||
export const QueryCount = defineQueryCount(sequelize);
|
||||
|
||||
export async function initDB() {
|
||||
try {
|
||||
// First try to connect to postgres directly to create database if needed
|
||||
const rootSequelize = new Sequelize('postgres', process.env.POSTGRES_USER, process.env.POSTGRES_PASSWORD, {
|
||||
host: process.env.POSTGRES_HOST || 'localhost',
|
||||
port: process.env.POSTGRES_PORT || 5432,
|
||||
dialect: 'postgres',
|
||||
logging: false
|
||||
});
|
||||
|
||||
try {
|
||||
// Try to create database if it doesn't exist
|
||||
await rootSequelize.query(`CREATE DATABASE ${process.env.POSTGRES_DB};`);
|
||||
console.log('Database did not exist, created.');
|
||||
} catch (err) {
|
||||
// Ignore error if database already exists
|
||||
if (!err.message.includes('already exists')) {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
await rootSequelize.close();
|
||||
}
|
||||
|
||||
// Now connect to the actual database
|
||||
await sequelize.authenticate();
|
||||
console.log('DB connected.');
|
||||
|
||||
// Get current database schema
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
const tables = await queryInterface.showAllTables();
|
||||
|
||||
if (!tables.includes('Files') || !tables.includes('QueryCounts')) {
|
||||
// If tables don't exist, create them
|
||||
console.log('DB doesn\'t exist, creating initial database schema...');
|
||||
await sequelize.sync();
|
||||
console.log('Database schema created.');
|
||||
|
||||
// Initialize QueryCount if it's a new installation
|
||||
await QueryCount.create({ count: 0 });
|
||||
} else {
|
||||
// Auto-migrate existing schema
|
||||
console.log('Checking for DB migrations...');
|
||||
await sequelize.sync({ alter: true });
|
||||
console.log('DB migrations completed.');
|
||||
}
|
||||
|
||||
// Only force sync if explicitly requested
|
||||
if (process.env.FORCE_FILE_REBUILD === '1') {
|
||||
await sequelize.sync({ force: true });
|
||||
console.log('DB forcefully synchronized.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unable to connect to the DB:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export default sequelize;
|
||||
139
lib/database/database.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Sequelize } from "sequelize";
|
||||
import "dotenv/config";
|
||||
|
||||
// Import models
|
||||
import defineFile from "./models/file.js";
|
||||
import defineQueryCount from "./models/queryCount.js";
|
||||
import defineMetadata from "./models/metadata.js";
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
process.env.POSTGRES_DB,
|
||||
process.env.POSTGRES_USER,
|
||||
process.env.POSTGRES_PASSWORD,
|
||||
{
|
||||
host: process.env.POSTGRES_HOST || "localhost",
|
||||
port: process.env.POSTGRES_PORT || 5432,
|
||||
dialect: "postgres",
|
||||
logging: process.env.DEBUG === "1" ? console.log : false,
|
||||
}
|
||||
);
|
||||
|
||||
// Initialize models
|
||||
export const File = defineFile(sequelize);
|
||||
export const QueryCount = defineQueryCount(sequelize);
|
||||
export const Metadata = defineMetadata(sequelize);
|
||||
Metadata.hasMany(File);
|
||||
File.belongsTo(Metadata, { as: "details" });
|
||||
|
||||
async function enableTrigram() {
|
||||
let query = `SELECT * from PG_extension where extname = 'pg_trgm'`;
|
||||
let [result] = await sequelize.query(query, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
});
|
||||
if (!result) {
|
||||
const enableTrigramQuery = `CREATE EXTENSION pg_trgm`;
|
||||
await sequelize.query(enableTrigramQuery);
|
||||
}
|
||||
//check if trigram index exists and create it.
|
||||
query = `
|
||||
SELECT
|
||||
t.relname AS table_name,
|
||||
i.relname AS index_name,
|
||||
a.attname AS column_name
|
||||
FROM
|
||||
pg_class t,
|
||||
pg_class i,
|
||||
pg_index ix,
|
||||
pg_attribute a
|
||||
WHERE
|
||||
t.oid = ix.indrelid
|
||||
AND i.oid = ix.indexrelid
|
||||
AND a.attrelid = t.oid
|
||||
AND a.attnum = ANY(ix.indkey)
|
||||
AND t.relkind = 'r'
|
||||
AND t.relname = 'Metadata'
|
||||
AND i.relname like 'trgm_%'
|
||||
ORDER BY
|
||||
t.relname,
|
||||
i.relname
|
||||
`;
|
||||
[result] = await sequelize.query(query, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
});
|
||||
if (!result) {
|
||||
let query = `CREATE INDEX trgm_t_idx ON "Metadata" USING GIN (title gin_trgm_ops)`;
|
||||
await sequelize.query(query);
|
||||
query = `CREATE INDEX trgm_at_idx ON "Metadata" USING GIN (alternatetitles gin_trgm_ops)`;
|
||||
await sequelize.query(query);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initDB() {
|
||||
try {
|
||||
// First try to connect to postgres directly to create database if needed
|
||||
const rootSequelize = new Sequelize(
|
||||
"postgres",
|
||||
process.env.POSTGRES_USER,
|
||||
process.env.POSTGRES_PASSWORD,
|
||||
{
|
||||
host: process.env.POSTGRES_HOST || "localhost",
|
||||
port: process.env.POSTGRES_PORT || 5432,
|
||||
dialect: "postgres",
|
||||
logging: false,
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
// Try to create database if it doesn't exist
|
||||
await rootSequelize.query(`CREATE DATABASE ${process.env.POSTGRES_DB};`);
|
||||
console.log("Database did not exist, created.");
|
||||
} catch (err) {
|
||||
// Ignore error if database already exists
|
||||
if (!err.message.includes("already exists")) {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
await rootSequelize.close();
|
||||
}
|
||||
|
||||
// Now connect to the actual database
|
||||
await sequelize.authenticate();
|
||||
console.log("DB connected.");
|
||||
|
||||
// Get current database schema
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
const tables = await queryInterface.showAllTables();
|
||||
|
||||
if (!tables.includes("Files") || !tables.includes("QueryCounts")) {
|
||||
// If tables don't exist, create them
|
||||
console.log("DB doesn't exist, creating initial database schema...");
|
||||
await sequelize.sync();
|
||||
await enableTrigram();
|
||||
console.log("Database schema created.");
|
||||
|
||||
// Initialize QueryCount if it's a new installation
|
||||
await QueryCount.create({ count: 0 });
|
||||
} else {
|
||||
// Auto-migrate existing schema
|
||||
console.log("Checking for DB migrations...");
|
||||
await sequelize.sync({ alter: true });
|
||||
await enableTrigram();
|
||||
console.log("DB migrations completed.");
|
||||
}
|
||||
|
||||
// Only force sync if explicitly requested
|
||||
if (process.env.FORCE_FILE_REBUILD === "1") {
|
||||
let queryCount = (await QueryCount.findOne())?.count || 0;
|
||||
await sequelize.sync({ force: true });
|
||||
await enableTrigram();
|
||||
//restore query count after force sync
|
||||
await QueryCount.create({ count: queryCount });
|
||||
console.log("DB forcefully synchronized.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unable to connect to the DB:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export default sequelize;
|
||||
@@ -1,21 +1,21 @@
|
||||
import debugPrint from "./debugprint.js";
|
||||
import { bulkIndexFiles } from "./services/elasticsearch.js";
|
||||
import debugPrint from "../utility/printutils.js";
|
||||
import { bulkIndexFiles } from "../services/elasticsearch.js";
|
||||
import { File } from "./models/index.js";
|
||||
import { readFileSync } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import { Piscina, FixedQueue } from "piscina";
|
||||
import { timer } from "./time.js";
|
||||
import { Timer } from "../utility/time.js";
|
||||
|
||||
let piscina = new Piscina({
|
||||
filename: resolve("./lib", "dbkwworker.js"),
|
||||
filename: resolve("./lib/database/workers", "dbkwworker.js"),
|
||||
taskQueue: new FixedQueue(),
|
||||
});
|
||||
|
||||
const BATCH_SIZE = 1000;
|
||||
const BATCH_SIZE = 100;
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const relatedKwRoot = "../lib/json/relatedkeywords/";
|
||||
const relatedKwRoot = "../json/relatedkeywords/";
|
||||
const catKwPath = resolve(__dirname, relatedKwRoot + "categories.json");
|
||||
const nameKwpath = resolve(__dirname, relatedKwRoot + "names.json");
|
||||
const regionKwpath = resolve(__dirname, relatedKwRoot + "regions.json");
|
||||
@@ -28,23 +28,44 @@ const keywords = {
|
||||
};
|
||||
|
||||
export async function optimizeDatabaseKws() {
|
||||
let proctime = new timer();
|
||||
let proctime = new Timer();
|
||||
let changes = 0;
|
||||
console.log("Optimizing DB Keywords...");
|
||||
let dbLength = await File.count();
|
||||
let optimizeTasks = [];
|
||||
let resolvedTasks = [];
|
||||
for (let i = 0; i < dbLength; ) {
|
||||
singleLineStatus(`Optimizing Keywords: ${i} / ${dbLength}`);
|
||||
let result = await File.findAndCountAll({
|
||||
limit: BATCH_SIZE,
|
||||
offset: i,
|
||||
let promiseIndex = 0;
|
||||
let currentIndex = 0;
|
||||
let result = await File.findAll({
|
||||
order: ["id", "filename"],
|
||||
attributes: [
|
||||
"id",
|
||||
"filename",
|
||||
"filenamekws",
|
||||
"category",
|
||||
"categorykws",
|
||||
"subcategories",
|
||||
"subcategorieskws",
|
||||
"region",
|
||||
"regionkws",
|
||||
"type",
|
||||
"nongame"
|
||||
],
|
||||
});
|
||||
for (let x = 0; x < result.rows.length; x++) {
|
||||
debugPrint(`Submitting job for: ${result.rows[x]["filename"]}`);
|
||||
for (let i = 0; i < dbLength; ) {
|
||||
let loopIndexStart = i;
|
||||
singleLineStatus(
|
||||
`Optimizing Keywords: ${i} / ${dbLength} ${((i / dbLength) * 100).toFixed(
|
||||
2
|
||||
)}% (${proctime.elapsed()}) Optimized Rows: ${changes}`
|
||||
);
|
||||
|
||||
for (let x = i; x < currentIndex + BATCH_SIZE; x++) {
|
||||
if(x >= dbLength) break; //Abort abandon ship, otherwise we sink
|
||||
debugPrint(`Submitting job for: ${result[x].filename}`);
|
||||
let data = [];
|
||||
for (let column in keywords) {
|
||||
data[column] = result.rows[x][column];
|
||||
data[column] = result[x][column];
|
||||
}
|
||||
optimizeTasks.push(
|
||||
piscina
|
||||
@@ -61,23 +82,28 @@ export async function optimizeDatabaseKws() {
|
||||
);
|
||||
i++;
|
||||
}
|
||||
currentIndex = i;
|
||||
let settledTasks = await Promise.all(optimizeTasks);
|
||||
resolvedTasks.push(...settledTasks);
|
||||
debugPrint(`Resolving ${resolvedTasks.length} optimization tasks.`);
|
||||
for (let y = 0; y < resolvedTasks.length; y++) {
|
||||
let changed = false;
|
||||
for (let column in keywords) {
|
||||
if (result.rows[y][column + "kws"] == resolvedTasks[y][column + "kws"])
|
||||
if (
|
||||
result[promiseIndex][column + "kws"] ==
|
||||
resolvedTasks[y][column + "kws"]
|
||||
)
|
||||
continue;
|
||||
result.rows[y][column + "kws"] = resolvedTasks[y][column + "kws"];
|
||||
result[promiseIndex][column + "kws"] = resolvedTasks[y][column + "kws"];
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
result.rows[y].save();
|
||||
await result[promiseIndex].save();
|
||||
changes++;
|
||||
}
|
||||
promiseIndex++;
|
||||
}
|
||||
await bulkIndexFiles(result.rows);
|
||||
await bulkIndexFiles(result.slice(loopIndexStart, currentIndex));
|
||||
optimizeTasks = [];
|
||||
resolvedTasks = [];
|
||||
}
|
||||
@@ -50,6 +50,11 @@ export default function (sequelize) {
|
||||
},
|
||||
group: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
nongame: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
indexes: [
|
||||
@@ -59,6 +64,5 @@ export default function (sequelize) {
|
||||
{ fields: ['region'] }
|
||||
]
|
||||
});
|
||||
|
||||
return File;
|
||||
}
|
||||
1
lib/database/models/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { File, QueryCount, Metadata } from '../database.js';
|
||||
148
lib/database/models/metadata.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import { DataTypes, DATE } from "sequelize";
|
||||
|
||||
export default function (sequelize) {
|
||||
const Metadata = sequelize.define(
|
||||
"Metadata",
|
||||
{
|
||||
id: {
|
||||
//these will match the igdbid to make things a little easier
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
alternatetitles: {
|
||||
type: DataTypes.STRING(4096),
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING(16384),
|
||||
},
|
||||
rating: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
coverartid: {
|
||||
type: DataTypes.STRING(2048),
|
||||
},
|
||||
releasedate: {
|
||||
type: DataTypes.DATEONLY,
|
||||
},
|
||||
genre: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
},
|
||||
developers: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
},
|
||||
publishers: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
},
|
||||
gamemodes: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
},
|
||||
platforms: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
},
|
||||
screenshots: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
},
|
||||
videos: {
|
||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||
},
|
||||
titleVector: {
|
||||
type: DataTypes.TSVECTOR,
|
||||
allowNull: true,
|
||||
},
|
||||
alternatetitlesVector: {
|
||||
type: DataTypes.TSVECTOR,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
indexes: [
|
||||
{ fields: ["title"] },
|
||||
{
|
||||
name: "metadata_search_t_idx",
|
||||
using: "gin",
|
||||
fields: ["titleVector"],
|
||||
},
|
||||
{
|
||||
name: "metadata_search_at_idx",
|
||||
using: "gin",
|
||||
fields: ["alternatetitlesVector"],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
Metadata.beforeSave("addVector", async (instance) => {
|
||||
const title = instance.title || "";
|
||||
const alternateTitles =
|
||||
JSON.parse(instance.alternatetitles || "[]")
|
||||
const titles = Object.values(alternateTitles).join(', ')
|
||||
let query = `
|
||||
SELECT to_tsvector('english', $1)
|
||||
`;
|
||||
let [results] = await sequelize.query(query, {
|
||||
bind: [title],
|
||||
raw: true,
|
||||
});
|
||||
instance.titleVector = results[0].to_tsvector;
|
||||
query = `
|
||||
SELECT to_tsvector('english', $1)
|
||||
`;
|
||||
[results] = await sequelize.query(query, {
|
||||
bind: [titles],
|
||||
raw: true,
|
||||
});
|
||||
instance.alternatetitlesVector = results[0].to_tsvector;
|
||||
});
|
||||
|
||||
// Add a class method for full-text search
|
||||
Metadata.searchByText = async function (field, searchQuery, platform, limit = 1) {
|
||||
let platformClause = "";
|
||||
let limitClause = `LIMIT ${limit}`;
|
||||
if (platform && platform != "Others") {
|
||||
platformClause = `AND '${platform}' = ANY(platforms)`;
|
||||
}
|
||||
let fieldName = field + 'Vector'
|
||||
const query = `
|
||||
SELECT id FROM "Metadata"
|
||||
WHERE "${fieldName}" @@ plainto_tsquery('english', $1) ${platformClause}
|
||||
ORDER BY length(title) ${limitClause}
|
||||
`;
|
||||
return await sequelize.query(query, {
|
||||
bind: [searchQuery],
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
});
|
||||
};
|
||||
|
||||
Metadata.fuzzySearchByText = async function (
|
||||
field,
|
||||
searchQuery,
|
||||
fuzziness,
|
||||
platform,
|
||||
limit = 1
|
||||
) {
|
||||
fuzziness = fuzziness || 0.6;
|
||||
let platformClause = "";
|
||||
let limitClause = `LIMIT ${limit}`;
|
||||
if (platform && platform != "Others") {
|
||||
platformClause = `AND '${platform}' = ANY(platforms)`;
|
||||
}
|
||||
const query = `
|
||||
SELECT id FROM "Metadata"
|
||||
WHERE SIMILARITY($1, $2) > $3
|
||||
${platformClause}
|
||||
ORDER BY length(title) ${limitClause}
|
||||
`;
|
||||
|
||||
return await sequelize.query(query, {
|
||||
model: Metadata,
|
||||
bind: [field, searchQuery, fuzziness],
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
});
|
||||
};
|
||||
|
||||
return Metadata;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ToWords } from "to-words";
|
||||
import { getSample } from "./services/elasticsearch.js";
|
||||
import { getSample } from "../../services/elasticsearch.js";
|
||||
|
||||
const toWords = new ToWords({
|
||||
localeCode: "en-US",
|
||||
@@ -1,11 +0,0 @@
|
||||
export default function debugPrint(string){
|
||||
if(process.env.DEBUG == "1"){
|
||||
console.log(string)
|
||||
}
|
||||
}
|
||||
|
||||
export function debugPrintDir(string){
|
||||
if(process.env.DEBUG == "1"){
|
||||
console.dir(string)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import debugPrint from './debugprint.js'
|
||||
import debugPrint from '../utility/printutils.js'
|
||||
|
||||
// See https://emulatorjs.org/docs/systems for available cores
|
||||
const systemConfigs = {
|
||||
24
lib/images/consoleicons.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default class ConsoleIcons {
|
||||
constructor(consoleData){
|
||||
this.consoleData = consoleData
|
||||
}
|
||||
|
||||
getConsoleImage(console){
|
||||
return this.consoleData[console]?.icon
|
||||
}
|
||||
ifConsoleExists(console){
|
||||
return this.consoleData[console] ? true : false
|
||||
}
|
||||
createConsoleImage(console){
|
||||
//fixups
|
||||
console = console.replace('Sony PlayStation', 'PlayStation')
|
||||
console = console.replace('Microsoft Xbox', 'Xbox')
|
||||
console = console.replace(/^Xbox$/, 'Xbox Classic')
|
||||
console = console.replace(/^Nintendo Game Boy$/, 'Nintendo Game Boy/Color')
|
||||
console = console.replace('Nintendo Game Boy Color', 'Nintendo Game Boy/Color')
|
||||
if(this.ifConsoleExists(console)){
|
||||
return `<img class='console' src='/proxy-image?url=${encodeURIComponent(this.getConsoleImage(console))}'>`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
41
lib/images/flag.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import fs from "fs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const flagsDir = path.join(__dirname, "../../views/public/images/flags");
|
||||
|
||||
|
||||
export default class Flags {
|
||||
constructor() {
|
||||
this.flags = this.getAvailableFlags();
|
||||
this.basePath = '/public/images/flags/'
|
||||
}
|
||||
|
||||
getAvailableFlags() {
|
||||
try {
|
||||
return fs
|
||||
.readdirSync(flagsDir)
|
||||
.filter((file) => file.endsWith(".png"))
|
||||
.map((file) => path.basename(file, ".png"));
|
||||
} catch (error) {
|
||||
console.error("Error reading flags directory:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
ifFlagExists(string){
|
||||
return this.flags.includes(string)
|
||||
}
|
||||
|
||||
getFlagPath(string){
|
||||
return `${this.basePath}${string}.png`
|
||||
}
|
||||
|
||||
createFlag(string){
|
||||
if(this.ifFlagExists(string)){
|
||||
return `<img class="flag" src="${this.getFlagPath(string)}"></img>`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
95
lib/json/maps/igdb_platform.json
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"Linux": "IBM PC Compatible",
|
||||
"PC (Microsoft Windows)": "IBM PC Compatible",
|
||||
"PlayStation": "Sony PlayStation 1",
|
||||
"PlayStation 2": "Sony PlayStation 2",
|
||||
"PlayStation 3": "Sony PlayStation 3",
|
||||
"Xbox": "Microsoft Xbox",
|
||||
"Xbox 360": "Microsoft Xbox 360",
|
||||
"Commodore C64/128/MAX": "Commodore 64",
|
||||
"Amiga": "Commodore Amiga",
|
||||
"Nintendo Entertainment System": "Nintendo Entertainment System",
|
||||
"Nintendo DS": "Nintendo DS",
|
||||
"Nintendo GameCube": "Nintendo GameCube",
|
||||
"Game Boy Color": "Nintendo Game Boy Color",
|
||||
"Dreamcast": "Sega Dreamcast",
|
||||
"Game Boy Advance": "Nintendo Game Boy Advance",
|
||||
"Amstrad CPC": "Amstrad CPC",
|
||||
"ZX Spectrum": "Sinclair ZX Spectrum",
|
||||
"MSX": "Microsoft MSX",
|
||||
"Sega Mega Drive/Genesis": "Sega Mega Drive",
|
||||
"Sega 32X": "Sega 32X",
|
||||
"Sega Saturn": "Sega Saturn",
|
||||
"Game Boy": "Nintendo Game Boy",
|
||||
"iOS": "Mobile",
|
||||
"Sega Game Gear": "Sega Game Gear",
|
||||
"Nintendo 3DS": "Nintendo 3DS",
|
||||
"PlayStation Portable": "Sony PlayStation Portable",
|
||||
"Wii": "Nintendo Wii",
|
||||
"Wii U": "Nintendo Wii U",
|
||||
"N-Gage": "Nokia N-Gage",
|
||||
"PlayStation Vita": "Sony PlayStation Vita",
|
||||
"3DO Interactive Multiplayer": "Panasonic 3DO",
|
||||
"Family Computer Disk System": "Nintendo Family Computer Disk System",
|
||||
"MSX2": "Microsoft MSX2",
|
||||
"Atari 7800": "Atari 7800",
|
||||
"Atari Lynx": "Atari Lynx",
|
||||
"Atari Jaguar": "Atari Jaguar",
|
||||
"Sega Master System/Mark III": "Sega Master System",
|
||||
"Atari 8-bit": "Atari 8-bit",
|
||||
"Atari 5200": "Atari 5200",
|
||||
"Intellivision": "Mattel Intellivision",
|
||||
"Vectrex": "GCE Vectrex",
|
||||
"Commodore VIC-20": "Commodore VIC-20",
|
||||
"Sharp X1": "Sharp X1",
|
||||
"Sega CD": "Sega CD",
|
||||
"Neo Geo MVS": "SNK NeoGeo Pocket",
|
||||
"SG-1000": "Sega SG-1000",
|
||||
"TurboGrafx-16/PC Engine": "NEC PC Engine",
|
||||
"Virtual Boy": "Nintendo Virtual Boy",
|
||||
"Microvision": "Microvision",
|
||||
"Bally Astrocade": "Bally Astrocade",
|
||||
"Commodore Plus/4": "Commodore Plus-4",
|
||||
"Apple IIGS": "Apple IIGS",
|
||||
"Philips CD-i": "Philips CD-i",
|
||||
"Neo Geo Pocket": "SNK NeoGeo Pocket",
|
||||
"Neo Geo Pocket Color": "SNK NeoGeo Pocket Color",
|
||||
"Sharp X68000": "Sharp X68000",
|
||||
"Fairchild Channel F": "Fairchild Channel F",
|
||||
"PC Engine SuperGrafx": "NEC PC Engine",
|
||||
"Texas Instruments TI-99": "Texas Instruments TI-99-4A",
|
||||
"Odyssey 2 / Videopac G7000": "Magnavox Odyssey 2",
|
||||
"Neo Geo CD": "SNK NeoGeo Pocket",
|
||||
"New Nintendo 3DS": "New Nintendo 3DS",
|
||||
"PC-9800 Series": "NEC PC-98",
|
||||
"FM-7": "Fujitsu FM-7",
|
||||
"Pokémon mini": "Nintendo Pokemon Mini",
|
||||
"PlayStation 5": "Sony PlayStation 5",
|
||||
"Xbox Series X|S": "Microsoft Xbox Series X|S",
|
||||
"Google Stadia": "Google",
|
||||
"DVD Player": "DVD-Video",
|
||||
"Blu-ray Player": "BD-Video",
|
||||
"Zeebo": "Zeebo",
|
||||
"PC-FX": "NEC PC-FX",
|
||||
"Game & Watch": "Nintendo Game & Watch",
|
||||
"Sega Pico": "Sega PICO",
|
||||
"Sinclair ZX81": "Sinclair ZX Spectrum",
|
||||
"Sharp MZ-2200": "Sharp MZ-2200",
|
||||
"Epoch Cassette Vision": "Epoch Game Pocket Computer",
|
||||
"Epoch Super Cassette Vision": "Epoch Super Cassette Vision",
|
||||
"Game.com": "Tiger Game.com",
|
||||
"Casio Loopy": "Casio Loopy",
|
||||
"Mega Duck/Cougar Boy": "Welback Mega Duck",
|
||||
"Leapster": "LeapFrog Leapster",
|
||||
"Leapster Explorer/LeadPad Explorer": "LeapFrog LeapPad",
|
||||
"Watara/QuickShot Supervision": "Watara SuperVision",
|
||||
"64DD": "Nintendo 64DD",
|
||||
"Arduboy": "Arduboy Inc Arduboy",
|
||||
"V.Smile": "VTech V.Smile",
|
||||
"Arcadia 2001": "Emerson Arcadia 2001",
|
||||
"Gizmondo": "Tiger Gizmondo",
|
||||
"Apple Pippin": "Apple-Bandai Pippin",
|
||||
"Panasonic M2": "Panasonic M2",
|
||||
"Super A'Can": "Funtech Super Acan",
|
||||
"Sega CD 32X": "Sega CD"
|
||||
}
|
||||
18
lib/json/maps/name_localization.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"en": "English",
|
||||
"ko": "Korea",
|
||||
"ar": "Arabic",
|
||||
"bn": "Bengali",
|
||||
"hi": "Hindi",
|
||||
"ru": "Russian",
|
||||
"tr": "Turkish",
|
||||
"pl": "Polish",
|
||||
"it": "Italian",
|
||||
"de": "German",
|
||||
"es": "Spanish",
|
||||
"fr": "French",
|
||||
"ja": "Japan",
|
||||
"pt": "Portuguese",
|
||||
"romaji": "Japanese title - romanization",
|
||||
"zh": "Chinese"
|
||||
}
|
||||
@@ -10,5 +10,8 @@
|
||||
["littleendian", "little endian"],
|
||||
["pc88", "pc-88", "pc 88"],
|
||||
["dvd", "digital video disc", "digital versatile disc"],
|
||||
["bros", "brothers", "bros."]
|
||||
["bros", "brothers", "bros."],
|
||||
["&", "and"],
|
||||
["+", "plus"],
|
||||
[".hack", "dothack", "dot hack"]
|
||||
]
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
"Game Boy Advance",
|
||||
"Game Boy Color",
|
||||
"Game Boy",
|
||||
"Gamecube",
|
||||
"GameCube",
|
||||
"Kiosk Video Compact Flash",
|
||||
"Misc",
|
||||
"New Nintendo 3DS",
|
||||
@@ -1,16 +1,21 @@
|
||||
{
|
||||
"terms": [
|
||||
"7z",
|
||||
"action replay",
|
||||
"addon",
|
||||
"artwork",
|
||||
"audio",
|
||||
"beta",
|
||||
"box",
|
||||
"boxart",
|
||||
"cbr",
|
||||
"chd",
|
||||
"cheat",
|
||||
"config",
|
||||
"codebreaker",
|
||||
"cfg",
|
||||
"csv",
|
||||
"datel",
|
||||
"debug",
|
||||
"dlc",
|
||||
"document",
|
||||
@@ -21,17 +26,16 @@
|
||||
"figurine",
|
||||
"firmware",
|
||||
"guide",
|
||||
"hack",
|
||||
"html",
|
||||
"ini",
|
||||
"installer",
|
||||
"intro",
|
||||
"json",
|
||||
"jpg",
|
||||
"manual",
|
||||
"mod",
|
||||
"movie",
|
||||
"music",
|
||||
"mp4",
|
||||
"ost",
|
||||
"overlay",
|
||||
"patch",
|
||||
@@ -47,6 +51,7 @@
|
||||
"setup",
|
||||
"soundtrack",
|
||||
"sqlite",
|
||||
"swf",
|
||||
"terms",
|
||||
"tool",
|
||||
"trainer",
|
||||
@@ -1 +0,0 @@
|
||||
export { File, QueryCount } from '../database.js';
|
||||
@@ -1,9 +1,9 @@
|
||||
import debugPrint from "./debugprint.js";
|
||||
import debugPrint from "../utility/printutils.js";
|
||||
import {
|
||||
search as elasticSearch,
|
||||
getSuggestions as elasticSuggestions,
|
||||
} from "./services/elasticsearch.js";
|
||||
import { File } from "./models/index.js";
|
||||
} from "../services/elasticsearch.js";
|
||||
import { File, Metadata } from "../database/models/index.js";
|
||||
|
||||
export default class Searcher {
|
||||
constructor(fields) {
|
||||
@@ -30,7 +30,12 @@ export default class Searcher {
|
||||
}
|
||||
|
||||
findIndex(id) {
|
||||
return File.findByPk(id);
|
||||
return File.findByPk(id, {
|
||||
include: {
|
||||
model: Metadata,
|
||||
as: "details"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getIndexSize() {
|
||||
@@ -1,32 +1,13 @@
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import debugPrint from '../debugprint.js';
|
||||
import { File } from '../models/index.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { Client } from "@elastic/elasticsearch";
|
||||
import debugPrint from "../utility/printutils.js";
|
||||
import { File, Metadata } from "../database/models/index.js";
|
||||
import { Timer } from "../utility/time.js";
|
||||
|
||||
const client = new Client({
|
||||
node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200'
|
||||
node: process.env.ELASTICSEARCH_URL || "http://localhost:9200",
|
||||
});
|
||||
|
||||
const INDEX_NAME = 'myrient_files';
|
||||
|
||||
// Cache for nonGameTerms
|
||||
let nonGameTermsCache = null;
|
||||
|
||||
function getNonGameTerms() {
|
||||
if (nonGameTermsCache) {
|
||||
return nonGameTermsCache;
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const nonGameTermsPath = resolve(__dirname, '../../lib/nonGameTerms.json');
|
||||
nonGameTermsCache = JSON.parse(readFileSync(nonGameTermsPath, 'utf8'));
|
||||
|
||||
return nonGameTermsCache;
|
||||
}
|
||||
const INDEX_NAME = "myrient_files";
|
||||
|
||||
export async function initElasticsearch() {
|
||||
try {
|
||||
@@ -40,56 +21,59 @@ export async function initElasticsearch() {
|
||||
analysis: {
|
||||
analyzer: {
|
||||
filename_analyzer: {
|
||||
type: 'custom',
|
||||
tokenizer: 'standard',
|
||||
filter: ['lowercase', 'word_delimiter_graph']
|
||||
}
|
||||
}
|
||||
}
|
||||
type: "custom",
|
||||
tokenizer: "standard",
|
||||
filter: ["lowercase", "word_delimiter_graph"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
properties: {
|
||||
filename: {
|
||||
type: 'text',
|
||||
analyzer: 'filename_analyzer'
|
||||
type: "text",
|
||||
analyzer: "filename_analyzer",
|
||||
},
|
||||
category: {
|
||||
type: 'text',
|
||||
analyzer: 'standard',
|
||||
type: "text",
|
||||
analyzer: "standard",
|
||||
fields: {
|
||||
keyword: {
|
||||
type: 'keyword'
|
||||
}
|
||||
}
|
||||
type: "keyword",
|
||||
},
|
||||
},
|
||||
},
|
||||
type: {
|
||||
type: 'text',
|
||||
analyzer: 'standard'
|
||||
type: "text",
|
||||
analyzer: "standard",
|
||||
},
|
||||
region: {
|
||||
type: 'text',
|
||||
analyzer: 'standard'
|
||||
type: "text",
|
||||
analyzer: "standard",
|
||||
},
|
||||
filenamekws: {
|
||||
type: 'text',
|
||||
analyzer: 'standard'
|
||||
type: "text",
|
||||
analyzer: "standard",
|
||||
},
|
||||
categorykws: {
|
||||
type: 'text',
|
||||
analyzer: 'standard'
|
||||
type: "text",
|
||||
analyzer: "standard",
|
||||
},
|
||||
regionkws: {
|
||||
type: 'text',
|
||||
analyzer: 'standard'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type: "text",
|
||||
analyzer: "standard",
|
||||
},
|
||||
nongame: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log('Elasticsearch index created');
|
||||
console.log("Elasticsearch index created");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Elasticsearch init error:', error);
|
||||
console.error("Elasticsearch init error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -99,16 +83,16 @@ export async function indexFile(file) {
|
||||
await client.index({
|
||||
index: INDEX_NAME,
|
||||
id: file.id.toString(),
|
||||
document: file
|
||||
document: file,
|
||||
});
|
||||
debugPrint(`Indexed file: ${file.filename}`);
|
||||
} catch (error) {
|
||||
console.error('Error indexing file:', error);
|
||||
console.error("Error indexing file:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function bulkIndexFiles(files) {
|
||||
const operations = files.flatMap(file => [
|
||||
const operations = files.flatMap((file) => [
|
||||
{ index: { _index: INDEX_NAME, _id: file.id.toString() } },
|
||||
{
|
||||
filename: file.filename,
|
||||
@@ -117,19 +101,20 @@ export async function bulkIndexFiles(files) {
|
||||
region: file.region,
|
||||
filenamekws: file.filenamekws,
|
||||
categorykws: file.categorykws,
|
||||
regionkws: file.regionkws
|
||||
}
|
||||
regionkws: file.regionkws,
|
||||
nongame: file.nongame,
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
const { errors, items } = await client.bulk({
|
||||
refresh: true,
|
||||
operations
|
||||
operations,
|
||||
});
|
||||
|
||||
if (errors) {
|
||||
console.error('Bulk indexing had errors');
|
||||
items.forEach(item => {
|
||||
console.error("Bulk indexing had errors");
|
||||
items.forEach((item) => {
|
||||
if (item.index.error) {
|
||||
console.error(item.index.error);
|
||||
}
|
||||
@@ -138,46 +123,53 @@ export async function bulkIndexFiles(files) {
|
||||
|
||||
debugPrint(`Bulk indexed ${files.length} files`);
|
||||
} catch (error) {
|
||||
console.error('Bulk indexing error:', error);
|
||||
console.error("Bulk indexing error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function search(query, options) {
|
||||
//add kws for selected fields
|
||||
let builtFields = []
|
||||
for(let field in options.fields){
|
||||
builtFields.push(options.fields[field])
|
||||
builtFields.push(options.fields[field] + 'kws')
|
||||
let builtFields = [];
|
||||
for (let field in options.fields) {
|
||||
builtFields.push(options.fields[field]);
|
||||
builtFields.push(options.fields[field] + "kws");
|
||||
}
|
||||
const searchQuery = {
|
||||
index: INDEX_NAME,
|
||||
body: {
|
||||
size: 1500,
|
||||
size: options.pageSize,
|
||||
from: options.pageSize * options.page,
|
||||
query: {
|
||||
bool: {
|
||||
must: buildMustClauses(query, options, builtFields),
|
||||
should: buildShouldClauses(query, options, builtFields)
|
||||
}
|
||||
should: buildShouldClauses(query, options, builtFields),
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
fields: {
|
||||
filename: {},
|
||||
category: {},
|
||||
type: {},
|
||||
region: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
region: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
if (options.hideNonGame) {
|
||||
searchQuery.body.query.bool["filter"] = {
|
||||
term: { nongame: false },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const startTime = process.hrtime();
|
||||
let timer = new Timer();
|
||||
const response = await client.search(searchQuery);
|
||||
|
||||
// Fetch full records from PostgreSQL for the search results
|
||||
const ids = response.hits.hits.map(hit => hit._id);
|
||||
const ids = response.hits.hits.map((hit) => hit._id);
|
||||
const fullRecords = await File.findAll({
|
||||
where: { id: ids }
|
||||
where: { id: ids },
|
||||
include: {model: Metadata, as: "details"},
|
||||
});
|
||||
|
||||
// Create a map of full records by id
|
||||
@@ -187,49 +179,45 @@ export async function search(query, options) {
|
||||
}, {});
|
||||
|
||||
// Build results with full PostgreSQL records
|
||||
let results = response.hits.hits.map(hit => ({
|
||||
...recordMap[hit._id].dataValues,
|
||||
let results = response.hits.hits.map((hit) => ({
|
||||
file: {
|
||||
...recordMap[hit._id]?.dataValues
|
||||
},
|
||||
metadata: {
|
||||
...recordMap[hit._id]?.details?.dataValues
|
||||
},
|
||||
score: hit._score,
|
||||
highlights: hit.highlight
|
||||
highlights: hit.highlight,
|
||||
}));
|
||||
|
||||
// Apply non-game content filtering in JavaScript if the option is enabled
|
||||
if (options.hideNonGame) {
|
||||
const nonGameTerms = getNonGameTerms();
|
||||
const termPatterns = nonGameTerms.terms.map(term => new RegExp(term, 'i'));
|
||||
|
||||
// Filter results in JavaScript (much faster than complex Elasticsearch queries)
|
||||
results = results.filter(item => {
|
||||
// Check if filename contains any of the non-game terms
|
||||
return !termPatterns.some(pattern => pattern.test(item.filename));
|
||||
});
|
||||
}
|
||||
|
||||
const elapsed = parseHrtimeToSeconds(process.hrtime(startTime));
|
||||
const elapsed = timer.elapsedSeconds();
|
||||
return {
|
||||
items: results,
|
||||
elapsed
|
||||
count: response.hits.total.value || 0,
|
||||
elapsed,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
return { items: [], elapsed: 0 };
|
||||
console.error("Search error:", error);
|
||||
return { items: [], elapsed: 0, count: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
function buildMustClauses(query, options, builtFields) {
|
||||
const clauses = [];
|
||||
|
||||
if (options.combineWith === 'AND') {
|
||||
query.split(' ').forEach(term => {
|
||||
if (options.combineWith === "AND") {
|
||||
query.split(" ").forEach((term) => {
|
||||
clauses.push({
|
||||
multi_match: {
|
||||
query: term,
|
||||
fields: builtFields.map(field =>
|
||||
field === 'filename' || 'filenamekws' ? `${field}^2` : field
|
||||
fields: builtFields.map((field) =>
|
||||
field === "filename" || field === "filenamekws"
|
||||
? `${field}^2`
|
||||
: field
|
||||
),
|
||||
fuzziness: options.fuzzy || 0,
|
||||
type: 'best_fields'
|
||||
}
|
||||
type: "best_fields",
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -240,54 +228,62 @@ function buildMustClauses(query, options, builtFields) {
|
||||
function buildShouldClauses(query, options, builtFields) {
|
||||
const clauses = [];
|
||||
|
||||
if (options.combineWith !== 'AND') {
|
||||
if (options.combineWith !== "AND") {
|
||||
clauses.push({
|
||||
multi_match: {
|
||||
query,
|
||||
fields: builtFields.map(field =>
|
||||
field === 'filename' || 'filenamekws' ? `${field}^2` : field
|
||||
fields: builtFields.map((field) =>
|
||||
field === "filename" || field === "filenamekws" ? `${field}^2` : field
|
||||
),
|
||||
fuzziness: options.fuzzy || 0,
|
||||
type: 'best_fields'
|
||||
}
|
||||
type: "best_fields",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return clauses;
|
||||
}
|
||||
|
||||
function parseHrtimeToSeconds(hrtime) {
|
||||
return (hrtime[0] + (hrtime[1] / 1e9)).toFixed(3);
|
||||
}
|
||||
|
||||
export async function getSuggestions(query, options) {
|
||||
try {
|
||||
const response = await client.search({
|
||||
index: INDEX_NAME,
|
||||
body: {
|
||||
query: {
|
||||
bool:{
|
||||
must: {
|
||||
multi_match: {
|
||||
query,
|
||||
fields: ['filename^2', 'filenamekws^2', 'category', 'categorykws'],
|
||||
fuzziness: 'AUTO',
|
||||
type: 'best_fields'
|
||||
fields: ["filename^2", "filenamekws^2", "category", "categorykws"],
|
||||
fuzziness: "AUTO",
|
||||
type: "best_fields",
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
term:{
|
||||
nongame: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_source: ['filename', 'category'],
|
||||
size: 10
|
||||
}
|
||||
_source: ["filename", "category"],
|
||||
size: 30,
|
||||
},
|
||||
});
|
||||
|
||||
return response.hits.hits.map(hit => ({
|
||||
suggestion: hit._source.filename
|
||||
}));
|
||||
let suggestions = response.hits.hits.map((hit) =>
|
||||
normalizeName(hit._source.filename),
|
||||
);
|
||||
return [...new Set(suggestions)].map(suggestion => ({
|
||||
suggestion: suggestion
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Suggestion error:', error);
|
||||
console.error("Suggestion error:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSample(query, options){
|
||||
export async function getSample(query, options) {
|
||||
try {
|
||||
const response = await client.search({
|
||||
index: INDEX_NAME,
|
||||
@@ -295,18 +291,29 @@ export async function getSample(query, options){
|
||||
query: {
|
||||
match: {
|
||||
filename: query,
|
||||
}
|
||||
},
|
||||
_source: ['filename'],
|
||||
size: 30
|
||||
}
|
||||
},
|
||||
_source: ["filename"],
|
||||
size: 30,
|
||||
},
|
||||
});
|
||||
|
||||
return response.hits.hits.map(hit => ({
|
||||
sample: hit._source.filename
|
||||
return response.hits.hits.map((hit) => ({
|
||||
sample: hit._source.filename,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Sample error:', error);
|
||||
console.error("Sample error:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeName(filename) {
|
||||
if (!filename) return;
|
||||
return filename
|
||||
.replace(
|
||||
/\.[A-z]{3,3}|\.|&|-|\+|,|v[0-9]+\.[0-9]+|\[.*?\]|\(.*?\)|the|usa/gi,
|
||||
""
|
||||
)
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim();
|
||||
}
|
||||
21
lib/utility/printutils.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export default function debugPrint(string){
|
||||
if(process.env.DEBUG == "1"){
|
||||
console.log(string)
|
||||
}
|
||||
}
|
||||
|
||||
export function debugPrintDir(string){
|
||||
if(process.env.DEBUG == "1"){
|
||||
console.dir(string)
|
||||
}
|
||||
}
|
||||
|
||||
export function singleLineStatus(string){
|
||||
if(process.stdout.isTTY && process.env.DEBUG != "1"){
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(string);
|
||||
} else {
|
||||
console.log(string);
|
||||
}
|
||||
}
|
||||
90
lib/utility/taskqueue.js
Normal file
@@ -0,0 +1,90 @@
|
||||
export default class TaskQueue {
|
||||
constructor(
|
||||
maxTasksPerSecond = 4,
|
||||
maxSimultaneousTasks = 8,
|
||||
maxQueueLength = 20
|
||||
) {
|
||||
this.maxTasksPerSecond = maxTasksPerSecond;
|
||||
this.maxQueueLength = maxQueueLength;
|
||||
this.maxSimultaneousTasks = maxSimultaneousTasks;
|
||||
this.queue = [];
|
||||
this.processing = false;
|
||||
this.lastProcessTime = 0;
|
||||
this.taskCount = 0;
|
||||
this.tasksWaiting = 0;
|
||||
}
|
||||
|
||||
async enqueue(taskFunction, that = this, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.queue.length >= this.maxQueueLength) {
|
||||
reject(new Error("Queue is full. Maximum queue size exceeded."));
|
||||
return;
|
||||
}
|
||||
this.queue.push({
|
||||
taskFunction,
|
||||
that,
|
||||
args,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
|
||||
if (!this.processing) {
|
||||
this.processQueue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async processQueue() {
|
||||
if (this.processing || this.queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.processing = true;
|
||||
|
||||
while (this.queue.length > 0) {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - this.lastProcessTime >= 1000) {
|
||||
this.taskCount = 0;
|
||||
this.lastProcessTime = now;
|
||||
}
|
||||
|
||||
if (
|
||||
this.taskCount >= this.maxTasksPerSecond ||
|
||||
this.tasksWaiting >= this.maxSimultaneousTasks
|
||||
) {
|
||||
const waitTime = 1000 - (now - this.lastProcessTime);
|
||||
await this.sleep(waitTime);
|
||||
continue;
|
||||
}
|
||||
|
||||
const task = this.queue.shift();
|
||||
this.taskCount++;
|
||||
this.tasksWaiting++;
|
||||
|
||||
try {
|
||||
const result = await task.taskFunction.apply(task.that, task.args);
|
||||
this.tasksWaiting--;
|
||||
task.resolve(result);
|
||||
} catch (error) {
|
||||
task.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
this.processing = false;
|
||||
}
|
||||
|
||||
sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
queueLength: this.queue.length,
|
||||
maxQueueSize: this.maxQueueSize,
|
||||
tasksPerSecond: this.maxTasksPerSecond,
|
||||
currentTaskCount: this.taskCount,
|
||||
isProcessing: this.processing,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export class timer {
|
||||
export class Timer {
|
||||
constructor() {
|
||||
this.startTime = process.hrtime();
|
||||
}
|
||||
@@ -9,8 +9,11 @@ export class timer {
|
||||
elapsed() {
|
||||
let elapsed = this.parseHrtimetoSeconds(process.hrtime(this.startTime));
|
||||
let h = Math.floor(elapsed / 3600);
|
||||
let m = Math.floor(elapsed / 60);
|
||||
let m = Math.floor((elapsed / 60) % 60);
|
||||
let s = Math.floor(elapsed % 60);
|
||||
return `${h ? h + "h" : ""}${m ? m + "m" : ""}${s + "s"}`;
|
||||
}
|
||||
elapsedSeconds(){
|
||||
return this.parseHrtimetoSeconds(process.hrtime(this.startTime));
|
||||
}
|
||||
}
|
||||
81
package-lock.json
generated
@@ -6,13 +6,13 @@
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.12.2",
|
||||
"@phalcode/ts-igdb-client": "^1.0.22",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.21.1",
|
||||
"figlet": "^1.7.0",
|
||||
"file-older-than": "^1.0.0",
|
||||
"i18n": "^0.15.1",
|
||||
"innertext": "^1.0.3",
|
||||
"jsdom": "^25.0.1",
|
||||
@@ -24,7 +24,7 @@
|
||||
"pg-hstore": "^2.3.4",
|
||||
"piscina": "^4.7.0",
|
||||
"sanitize": "^2.1.2",
|
||||
"sequelize": "^6.37.1",
|
||||
"sequelize": "^6.37.7",
|
||||
"sequelize-cli": "^6.6.2",
|
||||
"to-words": "^4.5.1",
|
||||
"uuid": "^11.1.0"
|
||||
@@ -449,6 +449,25 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phalcode/ts-apicalypse": {
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@phalcode/ts-apicalypse/-/ts-apicalypse-1.0.22.tgz",
|
||||
"integrity": "sha512-1l+2shlUXMsHuvcyLz/kG0VvjErarVCzx1bqJUmu0AHdCeXbDWKd/5bwIzC2Bdjkjk/qg5o3p8Ffb10arQ1oKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phalcode/ts-igdb-client": {
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@phalcode/ts-igdb-client/-/ts-igdb-client-1.0.22.tgz",
|
||||
"integrity": "sha512-UQuWjNXCCtbELOnqAzgoDAw8IAvV/4JqsF3NB1obL8IhDaDOClTK4WxhPmxGwmAyo4YnJyIQnCwmWjUnb2gFSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@phalcode/ts-apicalypse": "^1.0.22",
|
||||
"axios": "^1.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -650,6 +669,17 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -1280,12 +1310,6 @@
|
||||
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/duration-js": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/duration-js/-/duration-js-4.0.0.tgz",
|
||||
"integrity": "sha512-qoXjOsH97r+NrOa6sK5V2cwBOouVG/LI9jwgwKvjVkyqGpZ72yilWjjzFJYPqqbvNZDwpRMaLEUFE+PTefvOEA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -1603,15 +1627,6 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-older-than": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-older-than/-/file-older-than-1.0.0.tgz",
|
||||
"integrity": "sha512-OUYyochwjcAdahP6NPc/dMhBjkR9lUjMoJL+J9/woJljCGye7Io4R0Er4scguzZFYb2/tnxp12eqqPRusxZ6DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"duration-js": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
@@ -1678,6 +1693,26 @@
|
||||
"integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||
@@ -2902,6 +2937,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -3134,9 +3175,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sequelize": {
|
||||
"version": "6.37.5",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.5.tgz",
|
||||
"integrity": "sha512-10WA4poUb3XWnUROThqL2Apq9C2NhyV1xHPMZuybNMCucDsbbFuKg51jhmyvvAUyUqCiimwTZamc3AHhMoBr2Q==",
|
||||
"version": "6.37.7",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
|
||||
"integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.12.2",
|
||||
"@phalcode/ts-igdb-client": "^1.0.22",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.21.1",
|
||||
"figlet": "^1.7.0",
|
||||
"file-older-than": "^1.0.0",
|
||||
"i18n": "^0.15.1",
|
||||
"innertext": "^1.0.3",
|
||||
"jsdom": "^25.0.1",
|
||||
@@ -19,7 +19,7 @@
|
||||
"pg-hstore": "^2.3.4",
|
||||
"piscina": "^4.7.0",
|
||||
"sanitize": "^2.1.2",
|
||||
"sequelize": "^6.37.1",
|
||||
"sequelize": "^6.37.7",
|
||||
"sequelize-cli": "^6.6.2",
|
||||
"to-words": "^4.5.1",
|
||||
"uuid": "^11.1.0"
|
||||
|
||||
112
server.js
@@ -1,37 +1,46 @@
|
||||
import getAllFiles from "./lib/dircrawl.js";
|
||||
import FileHandler from "./lib/filehandler.js";
|
||||
import Searcher from "./lib/search.js";
|
||||
import getAllFiles from "./lib/crawler/dircrawl.js";
|
||||
import { optimizeDatabaseKws } from "./lib/database/dboptimize.js";
|
||||
import FileHandler from "./lib/crawler/filehandler.js";
|
||||
import Searcher from "./lib/search/search.js";
|
||||
import cron from "node-cron";
|
||||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import http from "http";
|
||||
import sanitize from "sanitize";
|
||||
import debugPrint from "./lib/debugprint.js";
|
||||
import debugPrint from "./lib/utility/printutils.js";
|
||||
import compression from "compression";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { generateAsciiArt } from "./lib/asciiart.js";
|
||||
import { generateAsciiArt } from "./lib/utility/asciiart.js";
|
||||
import {
|
||||
getEmulatorConfig,
|
||||
isEmulatorCompatible,
|
||||
isNonGameContent,
|
||||
} from "./lib/emulatorConfig.js";
|
||||
} from "./lib/emulator/emulatorConfig.js";
|
||||
import fetch from "node-fetch";
|
||||
import { initDB, File, QueryCount } from "./lib/database.js";
|
||||
import { initDB, File, QueryCount, Metadata } from "./lib/database/database.js";
|
||||
import { initElasticsearch } from "./lib/services/elasticsearch.js";
|
||||
import i18n, { locales } from "./config/i18n.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { optimizeDatabaseKws } from "./lib/dboptimize.js";
|
||||
import Flag from "./lib/images/flag.js";
|
||||
import ConsoleIcons from "./lib/images/consoleicons.js";
|
||||
import MetadataManager from "./lib/crawler/metadatamanager.js";
|
||||
|
||||
let categoryListPath = "./lib/categories.json";
|
||||
let nonGameTermsPath = "./lib/nonGameTerms.json";
|
||||
let emulatorsPath = "./lib/emulators.json";
|
||||
let categoryListPath = "./lib/json/terms/categories.json";
|
||||
let nonGameTermsPath = "./lib/json/terms/nonGameTerms.json";
|
||||
let emulatorsPath = "./lib/json/dynamic_content/emulators.json";
|
||||
let localeNamePath = "./lib/json/maps/name_localization.json";
|
||||
let categoryList = await FileHandler.parseJsonFile(categoryListPath);
|
||||
let nonGameTerms = await FileHandler.parseJsonFile(nonGameTermsPath);
|
||||
let emulatorsData = await FileHandler.parseJsonFile(emulatorsPath);
|
||||
let localeNames = await FileHandler.parseJsonFile(localeNamePath);
|
||||
let crawlTime = 0;
|
||||
let queryCount = 0;
|
||||
let fileCount = 0;
|
||||
let metadataMatchCount = 0;
|
||||
let indexPage = "pages/index";
|
||||
let flags = new Flag();
|
||||
let consoleIcons = new ConsoleIcons(emulatorsData);
|
||||
import { Op } from "sequelize";
|
||||
|
||||
// Initialize databases
|
||||
await initDB();
|
||||
@@ -41,6 +50,9 @@ await initElasticsearch();
|
||||
fileCount = await File.count();
|
||||
crawlTime = (await File.max("updatedAt"))?.getTime() || 0;
|
||||
queryCount = (await QueryCount.findOne())?.count || 0;
|
||||
metadataMatchCount = await File.count({
|
||||
where: { detailsId: { [Op.ne]: null } },
|
||||
});
|
||||
|
||||
let searchFields = ["filename", "category", "type", "region"];
|
||||
|
||||
@@ -51,6 +63,7 @@ let defaultSettings = {
|
||||
fuzzy: 0,
|
||||
prefix: true,
|
||||
hideNonGame: true,
|
||||
useOldResults: false,
|
||||
};
|
||||
|
||||
//programmatically set the default boosts while reducing overhead when adding another search field
|
||||
@@ -64,9 +77,11 @@ for (let field in searchFields) {
|
||||
}
|
||||
|
||||
let search = new Searcher(searchFields);
|
||||
let metadataManager = new MetadataManager();
|
||||
|
||||
async function getFilesJob() {
|
||||
console.log("Updating the file list.");
|
||||
let oldFileCount = fileCount || 0;
|
||||
fileCount = await getAllFiles(categoryList);
|
||||
if (!fileCount) {
|
||||
console.log("File update failed");
|
||||
@@ -74,6 +89,23 @@ async function getFilesJob() {
|
||||
}
|
||||
crawlTime = Date.now();
|
||||
console.log(`Finished updating file list. ${fileCount} found.`);
|
||||
if ((await Metadata.count()) < (await metadataManager.getIGDBGamesCount())) {
|
||||
await metadataManager.syncAllMetadata();
|
||||
metadataMatchCount = await File.count({
|
||||
where: { detailsId: { [Op.ne]: null } },
|
||||
});
|
||||
}
|
||||
if (fileCount > oldFileCount) {
|
||||
await metadataManager.matchAllMetadata();
|
||||
}
|
||||
await optimizeDatabaseKws();
|
||||
//this is less important and needs to run last.
|
||||
if (fileCount > oldFileCount) {
|
||||
metadataManager.matchAllMetadata(true);
|
||||
}
|
||||
metadataMatchCount = await File.count({
|
||||
where: { detailsId: { [Op.ne]: null } },
|
||||
});
|
||||
}
|
||||
|
||||
function buildOptions(page, options) {
|
||||
@@ -84,6 +116,7 @@ let defaultOptions = {
|
||||
crawlTime: crawlTime,
|
||||
queryCount: queryCount,
|
||||
fileCount: fileCount,
|
||||
metadataMatchCount: metadataMatchCount,
|
||||
generateAsciiArt: generateAsciiArt,
|
||||
isEmulatorCompatible: isEmulatorCompatible,
|
||||
isNonGameContent: isNonGameContent,
|
||||
@@ -94,6 +127,7 @@ function updateDefaults() {
|
||||
defaultOptions.crawlTime = crawlTime;
|
||||
defaultOptions.queryCount = queryCount;
|
||||
defaultOptions.fileCount = fileCount;
|
||||
defaultOptions.metadataMatchCount = metadataMatchCount;
|
||||
}
|
||||
|
||||
let app = express();
|
||||
@@ -106,7 +140,7 @@ app.use((req, res, next) => {
|
||||
});
|
||||
|
||||
//static files
|
||||
app.use('/public', express.static('views/public'))
|
||||
app.use("/public", express.static("views/public"));
|
||||
|
||||
//middleware
|
||||
app.use(sanitize.middleware);
|
||||
@@ -168,7 +202,11 @@ app.get("/", function (req, res) {
|
||||
app.get("/search", async function (req, res) {
|
||||
let query = req.query.q ? req.query.q : "";
|
||||
let pageNum = parseInt(req.query.p);
|
||||
let urlPrefix = encodeURI(`/search?s=${req.query.s}&q=${req.query.q}&p=`);
|
||||
let urlPrefix = encodeURI(
|
||||
`/search?s=${req.query.s}&q=${req.query.q}${
|
||||
req.query.o ? "&o=" + req.query.o : ""
|
||||
}&p=`
|
||||
);
|
||||
pageNum = pageNum ? pageNum : 1;
|
||||
let settings = {};
|
||||
try {
|
||||
@@ -193,22 +231,33 @@ app.get("/search", async function (req, res) {
|
||||
if (settings.combineWith != "AND") {
|
||||
delete settings.combineWith;
|
||||
}
|
||||
let results = await search.findAllMatches(query, settings);
|
||||
let loadOldResults =
|
||||
req.query.old === "true" || !(await Metadata.count()) ? true : false;
|
||||
settings.pageSize = loadOldResults ? 100 : 10;
|
||||
settings.page = pageNum - 1;
|
||||
settings.sort = req.query.o || "";
|
||||
let results = await search.findAllMatches(query.trim(), settings);
|
||||
debugPrint(results);
|
||||
if (results.items.length && pageNum == 1) {
|
||||
if (results.count && pageNum == 1) {
|
||||
queryCount += 1;
|
||||
await QueryCount.update({ count: queryCount }, { where: { id: 1 } });
|
||||
updateDefaults();
|
||||
}
|
||||
let options = {
|
||||
query: query,
|
||||
results: results,
|
||||
results: results.items,
|
||||
count: results.count,
|
||||
elapsed: results.elapsed,
|
||||
pageNum: pageNum,
|
||||
pageCount: Math.ceil(results.count / settings.pageSize),
|
||||
indexing: search.indexing,
|
||||
urlPrefix: urlPrefix,
|
||||
settings: settings,
|
||||
flags: flags,
|
||||
consoleIcons: consoleIcons,
|
||||
localeNames: localeNames,
|
||||
};
|
||||
let page = "results";
|
||||
let page = loadOldResults ? "resultsold" : "results";
|
||||
options = buildOptions(page, options);
|
||||
res.render(indexPage, options);
|
||||
});
|
||||
@@ -238,9 +287,10 @@ app.get("/lucky", async function (req, res) {
|
||||
updateDefaults();
|
||||
});
|
||||
|
||||
app.get("/settings", function (req, res) {
|
||||
app.get("/settings", async function (req, res) {
|
||||
let options = { defaultSettings: defaultSettings };
|
||||
let page = "settings";
|
||||
options.oldSettingsAvailable = (await Metadata.count()) ? true : false;
|
||||
options = buildOptions(page, options);
|
||||
res.render(indexPage, options);
|
||||
});
|
||||
@@ -292,6 +342,31 @@ app.get("/play/:id", async function (req, res) {
|
||||
res.render(indexPage, options);
|
||||
});
|
||||
|
||||
app.get("/info/:id", async function (req, res) {
|
||||
//set header to allow video embed
|
||||
res.setHeader("Cross-Origin-Embedder-Policy", "unsafe-non");
|
||||
let romId = parseInt(req.params.id);
|
||||
let romFile = await search.findIndex(romId);
|
||||
if (!romFile) {
|
||||
res.redirect("/");
|
||||
return;
|
||||
}
|
||||
let options = {
|
||||
file: {
|
||||
...romFile.dataValues,
|
||||
},
|
||||
metadata: {
|
||||
...romFile?.details?.dataValues,
|
||||
},
|
||||
flags: flags,
|
||||
consoleIcons: consoleIcons,
|
||||
localeNames: localeNames,
|
||||
};
|
||||
let page = "info";
|
||||
options = buildOptions(page, options);
|
||||
res.render(indexPage, options);
|
||||
});
|
||||
|
||||
app.get("/proxy-rom/:id", async function (req, res, next) {
|
||||
// Block access if emulator is disabled
|
||||
if (process.env.EMULATOR_ENABLED !== "true") {
|
||||
@@ -501,6 +576,7 @@ server.on("listening", function () {
|
||||
);
|
||||
});
|
||||
console.log(`Loaded ${fileCount} known files.`);
|
||||
console.log(`${metadataMatchCount} files contain matched metadata.`);
|
||||
|
||||
// Run file update job if needed
|
||||
if (
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
<a href="https://myrient.erista.me/donate/" class="btn btn-secondary"><%= __('about.donate') %></a>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 border-top pt-3">
|
||||
<h5><%= __('about.metadata.title') %></h5>
|
||||
<p><%= __('about.metadata.description', {metadata_source: 'IGDB'}) %></p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 border-top pt-3">
|
||||
<h5><%= __('about.emulator.title') %></h5>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
<%
|
||||
let ogPages = {
|
||||
"results": "opengraphresults",
|
||||
"info": "opengraphinfo"
|
||||
}
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -6,7 +12,7 @@
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<header>
|
||||
<%- include('../partials/header'); %>
|
||||
<%- page == 'results' ? include('../partials/opengraphresults') : include('../partials/opengraph') %>
|
||||
<%- ogPages[page] ? include(`../partials/${ogPages[page]}`) : include('../partials/opengraph') %>
|
||||
</header>
|
||||
|
||||
<%- include(page); %>
|
||||
@@ -25,6 +31,15 @@
|
||||
settingsElem.value = btoa(settingStore)
|
||||
}
|
||||
}
|
||||
if(typeof settingStore == 'string' && window.location.href){
|
||||
oldResultElem = document.getElementById('oldResults')
|
||||
if(oldResultElem){
|
||||
oldResultElem.value = JSON.parse(settingStore).useOldResults.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js'></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
|
||||
</html>
|
||||
118
views/pages/info.ejs
Normal file
@@ -0,0 +1,118 @@
|
||||
<%
|
||||
const titles = metadata.alternatetitles ? JSON.parse(metadata.alternatetitles) : []
|
||||
let title = metadata.title
|
||||
for(let x in titles){
|
||||
//display in language specific name if available
|
||||
if(x.includes(localeNames[locale])){
|
||||
title = titles[x]
|
||||
}
|
||||
}
|
||||
const coverarts = metadata.coverartid ? JSON.parse(metadata.coverartid) : []
|
||||
const coverartId = coverarts[file.region] || coverarts.default
|
||||
const coverUrl = coverartId ? `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_cover_big/${coverartId}.webp` : "/public/images/coverart/nocoverart.png"
|
||||
let images = []
|
||||
if(metadata.screenshots){
|
||||
images = metadata.screenshots.map((im) => `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_720p/${im}.webp`)
|
||||
}
|
||||
let videos = []
|
||||
if(metadata.videos){
|
||||
videos = metadata.videos
|
||||
}
|
||||
%>
|
||||
<link rel="stylesheet" href="/public/css/info.css">
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-10 col-xl-8">
|
||||
<div class="col-12 text-center">
|
||||
<h2 class="text-white"><%= title || file.filename %></h2>
|
||||
<p class="text-secondary text-platform"><%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %></p>
|
||||
</div>
|
||||
<div class="row ml-1">
|
||||
<img class="coverart col-md d-block mx-auto" src="<%= coverUrl %>" href="<%= file.path %>">
|
||||
<div class="col-md">
|
||||
<% if(metadata.rating) {%>
|
||||
<div>
|
||||
<%
|
||||
const fullstars = Math.floor(metadata.rating / 20)
|
||||
const halfstars = Math.floor(metadata.rating % 20 / 10)
|
||||
const nostars = 5 - fullstars - halfstars
|
||||
const nostarstring = '<i class="bi bi-star"></i>'
|
||||
const fullstarstring = '<i class="bi bi-star-fill"></i>'
|
||||
const halfstarstring = '<i class="bi bi-star-half"></i>'
|
||||
let stars = ''
|
||||
for(let x = 0; x < fullstars ; x++){
|
||||
stars += fullstarstring
|
||||
}
|
||||
if(halfstars){
|
||||
stars += halfstarstring
|
||||
}
|
||||
for(let x = 0; x < nostars; x++){
|
||||
stars += nostarstring
|
||||
}
|
||||
%>
|
||||
<p> <span class="stars"><%- stars%></span> (<%= Math.floor(metadata.rating) %>%)</p>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if(metadata.developers) {%>
|
||||
<div>
|
||||
<p><span class="info"><%= __('search.developed') %></span> <%= metadata.developers.join(", ") %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if(metadata.publishers) {%>
|
||||
<div>
|
||||
<p><span class="info"><%= __('search.published') %></span> <%= metadata.publishers.join(", ") %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if(metadata.releasedate) {%>
|
||||
<div>
|
||||
<p><span class="info"><%= __('search.release_date') %></span> <%= metadata.releasedate %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if(file.region) {%>
|
||||
<div>
|
||||
<p><span class="info"><%= __('search.region') %></span> <%= file.region %> <%- flags.createFlag(file.region) %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if(metadata.genre) {%>
|
||||
<div>
|
||||
<p><span class="info"><%= __('search.genre') %></span> <%= metadata.genre.join(", ") %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if(metadata.gamemodes) {%>
|
||||
<div>
|
||||
<p><span class="info"><%= __('search.modes') %></span> <%= metadata.gamemodes.join(", ") %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
<div>
|
||||
<p><span class="info"><%= __('search.filename') %></span> <%= file.filename %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p><span class="info"><%= __('search.size') %></span> <%= file.size %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p><span class="info"><%= __('search.upload_date') %></span> <%= file.date %></p>
|
||||
</div>
|
||||
<div class="">
|
||||
<p class="description"><%= metadata.description %></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="actions">
|
||||
<a class="btn btn-sm btn-secondary" href="<%= file.path %>"><%= __('search.download') %></a>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<% if (isEmulatorCompatible(file.category)) { %>
|
||||
<a class="btn btn-sm btn-secondary" href="/play/<%= file.id %>"><%= __('emulator.play')%></a></a> <% } else { %>
|
||||
<button class="btn btn-sm btn-secondary" disabled><%= __('emulator.not_available') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="right" title="<%= __('emulator.not_available_tooltip') %>"></i></button>
|
||||
<% }
|
||||
}%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% if(images.length || videos.length){ %>
|
||||
<div class="row col-md container mx-auto">
|
||||
<%- include("../partials/carousel", {images: images, videos: videos})%>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,21 +1,12 @@
|
||||
<%
|
||||
let pageCount = Math.ceil(results.items.length / 100)
|
||||
pageCount = pageCount ? pageCount : 1 //always ensure 1 page
|
||||
if(pageNum > pageCount){
|
||||
pageNum = 1
|
||||
}
|
||||
let entryStart = Math.floor((pageNum - 1) * 100)
|
||||
let entryEnd = entryStart + 100
|
||||
entryEnd = entryEnd > results.items.length ? results.items.length : entryEnd
|
||||
%>
|
||||
<script src='https://code.jquery.com/jquery-3.7.1.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/js/bootstrap.min.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdn.datatables.net/2.1.8/js/dataTables.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdn.datatables.net/2.1.8/js/dataTables.bootstrap4.js' crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/2.1.8/css/dataTables.bootstrap4.css" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/public/css/result.css">
|
||||
<div class="row w-100 m-0">
|
||||
<form class="ml-2 form-inline w-100" action="/search">
|
||||
<form id="searchform" class="ml-2 form-inline w-100" action="/search">
|
||||
<div class="w-100 align-items-center">
|
||||
<div class="form-group">
|
||||
<a href="/">
|
||||
@@ -25,13 +16,13 @@
|
||||
</a>
|
||||
<input type="hidden" name="s" id="searchSettings">
|
||||
<input id="search" type="text" class="w-50 form-control bg-dark text-white ml-2" name="q" value="<%= query %>" autocomplete="off" placeholder="<%= __('search.placeholder') %>">
|
||||
<button type="submit" class="btn btn-secondary ml-2"><%= __('search.button') %></button>
|
||||
<button type="submit" class="btn btn-secondary ml-2"><div id="loading" class="hidden spinner-border text-dark" role="status"></div><%= __('search.button') %></button>
|
||||
|
||||
</div>
|
||||
<ul class="SuggestionList col-sm-12" id="suggestionList" style="width: 50%;left: 195px;"></ul>
|
||||
</div>
|
||||
<p class="m-2">
|
||||
<%= __('search.found_plural', { count: results.items.length }) %> <%= __('search.in_seconds', { seconds: results.elapsed }) %>.
|
||||
<%= __('search.found_plural', { count: count }) %> <%= __('search.in_seconds', { seconds: elapsed }) %>.
|
||||
<%= indexing ? __('search.indexing') : "" %>
|
||||
<% if (settings.hideNonGame) { %>
|
||||
<span class="badge badge-info" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.hide_non_game.tooltip') %>">
|
||||
@@ -39,67 +30,19 @@
|
||||
<a href="/settings" class="text-white ml-1"><i class="bi bi-gear-fill"></i></a>
|
||||
</span>
|
||||
<% } %>
|
||||
<span class="badge badge-success" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.use_old_results.tooltip') %>">
|
||||
<%= __('search.new_experience') %>
|
||||
<a href="/settings" class="text-white ml-1"><i class="bi bi-gear-fill"></i></a>
|
||||
</span>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<div class="col-sm-12 w-100 mt-3">
|
||||
<p><%= __('search.displaying_results', { start: entryStart, end: entryEnd }) %></p>
|
||||
<table class="table text-white table-bordered" id="results">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= __('results.table.name') %></th>
|
||||
<th><%= __('results.table.group') %></th>
|
||||
<th><%= __('results.table.category') %></th>
|
||||
<th><%= __('results.table.region') %></th>
|
||||
<th><%= __('results.table.type') %></th>
|
||||
<th><%= __('results.table.size') %></th>
|
||||
<th><%= __('results.table.date') %></th>
|
||||
<th><%= __('results.table.score') %></th>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<th><%= __('results.table.play') %></th>
|
||||
<div>
|
||||
<% for (let x = 0; x < results.length; x++) { %>
|
||||
<%- include("../partials/result", {result: results[x]}) %>
|
||||
<% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<% for (let x = entryStart; x < entryEnd; x++) { %>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="<%= results.items[x].path %>">
|
||||
<%= results.items[x].filename %>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<%= results.items[x].group %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results.items[x].category %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results.items[x].region %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results.items[x].type %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results.items[x].size %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results.items[x].date %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results.items[x].score.toFixed(2) %>
|
||||
</td>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<td>
|
||||
<% if (isEmulatorCompatible(results.items[x].category)) { %>
|
||||
<a href="/play/<%= results.items[x].id %>" class="btn btn-sm btn-secondary"><%= __('emulator.play') %></a>
|
||||
<% } else { %>
|
||||
<button class="btn btn-sm btn-secondary" disabled><%= __('emulator.not_available') %></button>
|
||||
<% } %>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
</div>
|
||||
<%
|
||||
if(pageCount > 1) {
|
||||
%>
|
||||
@@ -109,7 +52,7 @@
|
||||
<nav aria-label="pagination">
|
||||
<ul class="pagination">
|
||||
<%
|
||||
let ellipsesElem = '<li class="dt-paging-button page-item disabled"><a class="page-link ellipsis" aria-controls="results" aria-disabled="true" data-dt-idx="ellipsis" tabindex="-1">…</a></li> '
|
||||
let ellipsesElem = '<li class="dt-paging-button page-item disabled"><a class="page-link ellipsis" aria-controls="results" aria-disabled="true" data-dt-idx="ellipsis" tabindex="-1">…</a></li>'
|
||||
let pageUrlPrefix = ''
|
||||
const getPageRange = (page) => {
|
||||
let pageUpperLimit = pageCount - 4
|
||||
@@ -137,7 +80,9 @@
|
||||
<li class="dt-paging-button page-item <%= pageNum == 1 ? 'disabled' : '' %>"><a <%= pageNum != 1 ? `href=${urlPrefix + (pageNum - 1)}` : '' %> class="page-link previous" aria-controls="results" aria-disabled="true" aria-label="Previous" data-dt-idx="previous" tabindex="-1">‹</a></li>
|
||||
<li class="dt-paging-button page-item <%= pageNum == 1 ? 'active' : '' %>"><a href="<%= urlPrefix + 1 %>" class="page-link" aria-controls="results" aria-current="page" data-dt-idx="0">1</a></li>
|
||||
<%- pageNum >= 5 ? ellipsesElem : '' %>
|
||||
<% for(let x = pageRange.lower; x <= pageRange.upper; x++){ %>
|
||||
<% for(let x = pageRange.lower; x <= pageRange.upper; x++){
|
||||
if(x == pageCount) break;
|
||||
%>
|
||||
<li class="dt-paging-button page-item <%= pageNum == x ? 'active' : '' %>"><a href="<%= urlPrefix + x %>" class="page-link" aria-controls="results" aria-current="page" data-dt-idx="<%= x - 1 %>"><%= x %></a></li>
|
||||
<% } %>
|
||||
<%- pageNum <= pageCount - 5 ? ellipsesElem : '' %>
|
||||
@@ -152,31 +97,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script defer>
|
||||
resultTable = new DataTable('#results', {
|
||||
"order": [[7, 'desc']],
|
||||
"columns": [
|
||||
{ "data": "name" }, // Name
|
||||
{ "data": "group" }, // Group
|
||||
{ "data": "category" }, // Category
|
||||
{ "data": "region" }, // Region
|
||||
{ "data": "type" }, // Type
|
||||
{ "data": "size" }, // Size
|
||||
{ "data": "date" }, // Date
|
||||
{ "data": "score" }, // Search Score
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
{ "data": "play", "orderable": false } // Play button column
|
||||
<% } %>
|
||||
],
|
||||
"lengthMenu": [100, { label: 'All', value: -1 }, 50, 25, 10],
|
||||
"paging": false,
|
||||
"filter": false,
|
||||
"layout": {
|
||||
"bottomStart": ''
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize tooltips
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
$( "#searchform" ).on( "submit", function( event ) {
|
||||
$("#loading").removeClass('hidden')
|
||||
});
|
||||
</script>
|
||||
187
views/pages/resultsold.ejs
Normal file
@@ -0,0 +1,187 @@
|
||||
<%
|
||||
pageCount = pageCount ? pageCount : 1 //always ensure 1 page
|
||||
if(pageNum > pageCount){
|
||||
pageNum = 1
|
||||
}
|
||||
%>
|
||||
<script src='https://code.jquery.com/jquery-3.7.1.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/js/bootstrap.min.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdn.datatables.net/2.1.8/js/dataTables.js' crossorigin="anonymous"></script>
|
||||
<script src='https://cdn.datatables.net/2.1.8/js/dataTables.bootstrap4.js' crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/2.1.8/css/dataTables.bootstrap4.css" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/public/css/resultsold.css">
|
||||
<div class="row w-100 m-0">
|
||||
<form class="ml-2 form-inline w-100" action="/search">
|
||||
<div class="w-100 align-items-center">
|
||||
<div class="form-group">
|
||||
<a href="/">
|
||||
<pre class="mt-4 ml-2" style="font: 6px / 5px monospace; color: white; text-align: center; overflow: hidden; display: inline-flex;">
|
||||
<%= generateAsciiArt() %>
|
||||
</pre>
|
||||
</a>
|
||||
<input type="hidden" name="s" id="searchSettings">
|
||||
<input id="search" type="text" class="w-50 form-control bg-dark text-white ml-2" name="q" value="<%= query %>" autocomplete="off" placeholder="<%= __('search.placeholder') %>">
|
||||
<button type="submit" class="btn btn-secondary ml-2"><div id="loading" class="hidden spinner-border text-dark" role="status"></div><%= __('search.button') %></button>
|
||||
|
||||
</div>
|
||||
<ul class="SuggestionList col-sm-12" id="suggestionList" style="width: 50%;left: 195px;"></ul>
|
||||
</div>
|
||||
<p class="m-2">
|
||||
<%= __('search.found_plural', { count: count }) %> <%= __('search.in_seconds', { seconds: elapsed }) %>.
|
||||
<%= indexing ? __('search.indexing') : "" %>
|
||||
<% if (settings.hideNonGame) { %>
|
||||
<span class="badge badge-info" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.hide_non_game.tooltip') %>">
|
||||
<%= __('search.non_game_filter') %>
|
||||
<a href="/settings" class="text-white ml-1"><i class="bi bi-gear-fill"></i></a>
|
||||
</span>
|
||||
<% } %>
|
||||
<span class="badge badge-success" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.use_old_results.tooltip') %>">
|
||||
<%= __('search.old_experience') %>
|
||||
<a href="/settings" class="text-white ml-1"><i class="bi bi-gear-fill"></i></a>
|
||||
</span>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<div class="col-sm-12 w-100 mt-3">
|
||||
<p><%= __('search.displaying_results', { start: ((pageNum - 1) * 100), end: pageNum * 100 < count ? pageNum * 100 : count }) %></p>
|
||||
<table class="table text-white table-bordered" id="results">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="" id="filename"><span><%= __('results.table.name') %></span><span class="dt-column-order"></span></th>
|
||||
<th class="" id="group"><span><%= __('results.table.group') %></span><span class="dt-column-order"></span></th>
|
||||
<th class="" id="category"><span><%= __('results.table.category') %></span><span class="dt-column-order"></span></th>
|
||||
<th class="" id="region"><span><%= __('results.table.region') %></span><span class="dt-column-order"></span></th>
|
||||
<th class="" id="type"><span><%= __('results.table.type') %></span><span class="dt-column-order"></span></th>
|
||||
<th class="" id="size"><span><%= __('results.table.size') %></span><span class="dt-column-order"></span></th>
|
||||
<th class="" id="date"><span><%= __('results.table.date') %></span><span class="dt-column-order"></span></th>
|
||||
<th class="" id="score"><span><%= __('results.table.score') %></span><span class="dt-column-order"></span></th>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<th><%= __('results.table.play') %></th>
|
||||
<% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<% for (let x = 0; x < results.length; x++) { %>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="<%= results[x].file.path %>">
|
||||
<%= results[x].file.filename %>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<%= results[x].file.group %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results[x].file.category %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results[x].file.region %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results[x].file.type %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results[x].file.size %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results[x].file.date %>
|
||||
</td>
|
||||
<td>
|
||||
<%= results[x].score.toFixed(2) %>
|
||||
</td>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<td>
|
||||
<% if (isEmulatorCompatible(results[x].category)) { %>
|
||||
<a href="/play/<%= results[x].id %>" class="btn btn-sm btn-secondary"><%= __('emulator.play') %></a>
|
||||
<% } else { %>
|
||||
<button class="btn btn-sm btn-secondary" disabled><%= __('emulator.not_available') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('emulator.not_available_tooltip') %>"></i></button>
|
||||
<% } %>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
<%
|
||||
if(pageCount > 1) {
|
||||
%>
|
||||
<div class="row justify-content-between">
|
||||
<div class="d-md-flex justify-content-between align-items-center dt-layout-end col-md-auto ml-auto">
|
||||
<div class="dt-paging">
|
||||
<nav aria-label="pagination">
|
||||
<ul class="pagination">
|
||||
<%
|
||||
let ellipsesElem = '<li class="dt-paging-button page-item disabled"><a class="page-link ellipsis" aria-controls="results" aria-disabled="true" data-dt-idx="ellipsis" tabindex="-1">…</a></li> '
|
||||
let pageUrlPrefix = ''
|
||||
const getPageRange = (page) => {
|
||||
let pageUpperLimit = pageCount - 4
|
||||
if(page > 4 && page < pageUpperLimit){
|
||||
return {
|
||||
lower: page,
|
||||
upper: page + 3
|
||||
}
|
||||
}
|
||||
else if(page <= 4){
|
||||
return {
|
||||
lower: 2,
|
||||
upper: pageCount >= 5 ? 5 : pageCount
|
||||
}
|
||||
}
|
||||
else if(page >= pageUpperLimit){
|
||||
return {
|
||||
lower: pageCount - 4,
|
||||
upper: pageCount - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
let pageRange = getPageRange(pageNum)
|
||||
%>
|
||||
<li class="dt-paging-button page-item <%= pageNum == 1 ? 'disabled' : '' %>"><a <%= pageNum != 1 ? `href=${urlPrefix + (pageNum - 1)}` : '' %> class="page-link previous" aria-controls="results" aria-disabled="true" aria-label="Previous" data-dt-idx="previous" tabindex="-1">‹</a></li>
|
||||
<li class="dt-paging-button page-item <%= pageNum == 1 ? 'active' : '' %>"><a href="<%= urlPrefix + 1 %>" class="page-link" aria-controls="results" aria-current="page" data-dt-idx="0">1</a></li>
|
||||
<%- pageNum >= 5 ? ellipsesElem : '' %>
|
||||
<% for(let x = pageRange.lower; x <= pageRange.upper; x++){
|
||||
if(x == pageCount) break;
|
||||
%>
|
||||
<li class="dt-paging-button page-item <%= pageNum == x ? 'active' : '' %>"><a href="<%= urlPrefix + x %>" class="page-link" aria-controls="results" aria-current="page" data-dt-idx="<%= x - 1 %>"><%= x %></a></li>
|
||||
<% } %>
|
||||
<%- pageNum <= pageCount - 5 ? ellipsesElem : '' %>
|
||||
<li class="dt-paging-button page-item <%= pageNum == pageCount ? 'active' : '' %>"><a href="<%= urlPrefix + pageCount %>" class="page-link" aria-controls="results" data-dt-idx="<%= pageCount - 1 %>"><%= pageCount %></a></li>
|
||||
<li class="dt-paging-button page-item <%= pageNum == pageCount ? 'disabled' : '' %>"><a <%= pageNum != pageCount ? `href=${urlPrefix + (pageNum + 1)}` : '' %> class="page-link next" aria-controls="results" aria-label="Next" data-dt-idx="next">›</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<script defer>
|
||||
// URLParams = new URLSearchParams(window.location.search);
|
||||
// if(!URLParams.get('o')){
|
||||
// // URLParams.set('o', 'score:asc');
|
||||
// // $('#score .dt-column-order').addClass('order-asc')
|
||||
// }
|
||||
// else{
|
||||
// let sorting = URLParams.get('o').split(':');
|
||||
// $(`#${sorting[0]} .dt-column-order`).addClass(`order-${sorting[1]}`)
|
||||
// }
|
||||
// $(".dt-orderable").each(function( i ){
|
||||
|
||||
// $( this ).on( "click", function (){
|
||||
// if($(this).find('.dt-column-order').hasClass('order-asc')){
|
||||
// URLParams.set('o', `${$(this).attr('id')}:desc`)
|
||||
// }
|
||||
// else if($(this).find('.dt-column-order').hasClass('order-desc')){
|
||||
// URLParams.delete('o')
|
||||
// }
|
||||
// else {
|
||||
// URLParams.set('o', `${$(this).attr('id')}:asc`)
|
||||
// }
|
||||
// window.location = location.protocol + '//' + location.host + location.pathname + '?' + URLParams.toString()
|
||||
// })
|
||||
// })
|
||||
</script>
|
||||
<script defer>
|
||||
$( "#searchform" ).on( "submit", function( event ) {
|
||||
$("#loading").removeClass('hidden')
|
||||
});
|
||||
</script>
|
||||
@@ -5,16 +5,22 @@
|
||||
<%= __('nav.search') %>!
|
||||
</pre>
|
||||
<div class="text-center text-white">
|
||||
<form>
|
||||
<form id="searchform">
|
||||
<input type="hidden" name="s" id="searchSettings">
|
||||
<input type="hidden" name="old" id="oldResults">
|
||||
<input id="search" type="text" style="width: 80%;display: inline;" class="form-control bg-dark text-white mb-2"
|
||||
name="q" autocomplete="off" placeholder="<%= __('search.placeholder') %>">
|
||||
<ul class="SuggestionList col-sm-12" id="suggestionList" style="width: 78%;left: 11%;"></ul>
|
||||
<div>
|
||||
<button type="submit" formaction="/search" class="btn btn-secondary"><%= __('search.button') %></button>
|
||||
<button type="submit" formaction="/search" class="btn btn-secondary"><div id="loading" class="hidden spinner-border text-dark" role="status"></div><%= __('search.button') %></button>
|
||||
<button type="submit" formaction="/lucky" class="btn btn-secondary"><%= __('search.lucky') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script defer>
|
||||
$( "#searchform" ).on( "submit", function( event ) {
|
||||
$("#loading").removeClass('hidden')
|
||||
});
|
||||
</script>
|
||||
@@ -1,6 +1,3 @@
|
||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js'></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
|
||||
<div class="row h-50 w-100 m-0">
|
||||
<div class="col-sm-12 my-auto text-center">
|
||||
<pre style="font: 20px / 19px monospace; color: white; text-align: center; overflow: hidden;">
|
||||
@@ -48,11 +45,15 @@
|
||||
<label class="checkbox-inline p-1">
|
||||
<input type="checkbox" id="combineWith" value="AND">
|
||||
<%= __('settings.extras.match_all.label') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.match_all.tooltip') %>"></i>
|
||||
</label>
|
||||
</label><br>
|
||||
<label class="checkbox-inline p-1">
|
||||
<input type="checkbox" id="hideNonGame" value="true">
|
||||
<%= __('settings.extras.hide_non_game.label') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.hide_non_game.tooltip') %>"></i>
|
||||
</label>
|
||||
<label class="checkbox-inline p-1">
|
||||
<input type="checkbox" id="useOldResults" value="true" <%= oldSettingsAvailable ? '' : 'disabled=""' %>">
|
||||
<%= __('settings.extras.use_old_results.label') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="top" title="<%= __('settings.extras.use_old_results.tooltip') %>"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
45
views/partials/carousel.ejs
Normal file
@@ -0,0 +1,45 @@
|
||||
<%
|
||||
if(typeof videos === 'undefined'){
|
||||
let videos = []
|
||||
}
|
||||
if(typeof images === 'undefined'){
|
||||
let images = []
|
||||
}
|
||||
%>
|
||||
<script src="/public/js/video.js" defer></script>
|
||||
<div id="carouselIndicators" class="carousel slide slideshow-container" data-ride="carousel" data-interval="10000">
|
||||
<ol class="carousel-indicators">
|
||||
<li data-target="#carouselIndicators" data-slide-to="0" class="active"></li>
|
||||
<% for(let x = 1; x < images.length; x++){%>
|
||||
<li data-target="#carouselIndicators" data-slide-to="<%= x %>"></li>
|
||||
<% } %>
|
||||
<% for(let x = 0; x < videos.length; x++){%>
|
||||
<li data-target="#carouselIndicators" data-slide-to="<%= x + images.length %>"></li>
|
||||
<% } %>
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
<img class="d-block w-100" src="<%= images[0] %>">
|
||||
</div>
|
||||
<% for(let x = 1; x < images.length; x++){%>
|
||||
<div class="carousel-item">
|
||||
<img class="d-block w-100" src="<%= images[x] %>">
|
||||
</div>
|
||||
<% } %>
|
||||
<% for(let x = 0; x < videos.length; x++){ %>
|
||||
<div class="carousel-item carousel-video">
|
||||
<div class="d-block col-md carousel-video-inner embed-responsive embed-responsive-16by9">
|
||||
<div class="video-player" id="player<%= x %>" data-video-id="<%= videos[x] %>"></div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carouselIndicators" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carouselIndicators" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -4,18 +4,15 @@
|
||||
<div class="stats"> | </div>
|
||||
<div id="file-count" class="stats"><%= __('footer.files') %></div>
|
||||
<div class="stats"> | </div>
|
||||
<div id="metadata-count" class="stats"><%= __('footer.metadata') %></div>
|
||||
<div class="stats"> | </div>
|
||||
<div id="crawl-time" class="stats"><%= __('footer.last_crawl') %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer>
|
||||
function timeConverter(UNIX_timestamp){
|
||||
var timestamp = parseInt(UNIX_timestamp)
|
||||
var date = new Date(timestamp);
|
||||
var options = { hour12: false };
|
||||
return date.toLocaleString(options)
|
||||
}
|
||||
document.getElementById('crawl-time').innerText += ` ${timeConverter('<%= crawlTime %>')}`
|
||||
document.getElementById('file-count').innerText += ` ${(<%= fileCount %>).toLocaleString(undefined)}`
|
||||
document.getElementById('metadata-count').innerText += ` ${(<%= metadataMatchCount %>).toLocaleString(undefined)}`
|
||||
document.getElementById('query-count').innerText += ` ${(<%= queryCount %>).toLocaleString(undefined)}`
|
||||
</script>
|
||||
|
||||
@@ -14,3 +14,4 @@
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
|
||||
<script src="/public/js/utility.js"></script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<meta name="description" content="Myrient Search is your go to search engine for searching through publicly available video game collections with the intention of keeping them from becoming lost to time.">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://myrientsearch.org">
|
||||
<meta property="og:url" content="https://<%= process.env.HOSTNAME %>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Myrient Search">
|
||||
<meta property="og:description" content="Myrient Search is your go to search engine for searching through publicly available video game collections with the intention of keeping them from becoming lost to time.">
|
||||
@@ -10,7 +10,7 @@
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="myrientsearch.org">
|
||||
<meta property="twitter:url" content="https://myrientsearch.org">
|
||||
<meta property="twitter:url" content="https://<%= process.env.HOSTNAME %>">
|
||||
<meta name="twitter:title" content="Myrient Search">
|
||||
<meta name="twitter:description" content="Myrient Search is your go to search engine for searching through publicly available video game collections with the intention of keeping them from becoming lost to time.">
|
||||
<meta name="twitter:image" content="https://opengraph.b-cdn.net/production/images/74980e84-25fb-4bb7-af42-243168815200.png?token=9ZpCsjaOVazfMIoiRI_xu28uQkYfDOMdOoGEc4c0KVc&height=630&width=1200&expires=33265501698">
|
||||
|
||||
23
views/partials/opengraphinfo.ejs
Normal file
@@ -0,0 +1,23 @@
|
||||
<%
|
||||
const coverarts = metadata.coverartid ? JSON.parse(metadata.coverartid) : []
|
||||
const coverartId = coverarts[file.region] || coverarts.default
|
||||
const coverUrl = coverartId ? `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_cover_big/${coverartId}.webp` : "/public/images/coverart/nocoverart.png"
|
||||
%>
|
||||
|
||||
<meta name="description" content="Myrient Search Info">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://<%= process.env.HOSTNAME %>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="<%= metadata.title || file.filename %>">
|
||||
<meta property="og:description" content="<%= metadata.description || "No description found." %>">
|
||||
<meta property="og:image" content="https://<%= process.env.HOSTNAME%>/<%=coverUrl%>">
|
||||
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="myrientsearch.org">
|
||||
<meta property="twitter:url" content="https://<%= process.env.HOSTNAME %>">
|
||||
<meta name="twitter:title" content="<%= metadata.title || file.filename %>">
|
||||
<meta name="twitter:description" content="<%= metadata.description || "No description found." %>">
|
||||
<meta name="twitter:image" content="https://<%= process.env.HOSTNAME %>/<%=coverUrl%>">
|
||||
@@ -1,16 +1,15 @@
|
||||
<%
|
||||
let resultStart = Math.floor((pageNum - 1) * 100)
|
||||
let length = results.items.length > 5 + resultStart ? 5 + resultStart : results.items.length
|
||||
let length = results.length > 5 ? 5 : results.length
|
||||
let resultString = ''
|
||||
for(let x = resultStart ; x < length; x++){
|
||||
resultString += `${x + 1}: ${results.items[x].filename}\n\n`
|
||||
for(let x = 0 ; x < length; x++){
|
||||
resultString += `${x + 1}: ${results[x].file.filename}\n\n`
|
||||
}
|
||||
resultString = resultString.trim()
|
||||
%>
|
||||
<meta name="description" content="Myrient Search Results">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://myrientsearch.org">
|
||||
<meta property="og:url" content="https://<%= process.env.HOSTNAME %>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Myrient Search Results: <%= query %>">
|
||||
<meta property="og:description" content="<%= resultString %>">
|
||||
@@ -18,6 +17,6 @@ resultString = resultString.trim()
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="myrientsearch.org">
|
||||
<meta property="twitter:url" content="https://myrientsearch.org">
|
||||
<meta property="twitter:url" content="https://<%= process.env.HOSTNAME %>">
|
||||
<meta name="twitter:title" content="Myrient Search Results: <%= query %>">
|
||||
<meta name="twitter:description" content="<%= resultString %>">
|
||||
|
||||
49
views/partials/result.ejs
Normal file
@@ -0,0 +1,49 @@
|
||||
<%
|
||||
const metadata = result.metadata || new Object()
|
||||
const file = result.file || new Object()
|
||||
const titles = metadata.alternatetitles ? JSON.parse(metadata.alternatetitles) : []
|
||||
let title = metadata.title
|
||||
for(let x in titles){
|
||||
//display in language specific name if available
|
||||
if(x.includes(localeNames[locale])){
|
||||
title = titles[x]
|
||||
}
|
||||
}
|
||||
const coverarts = metadata.coverartid ? JSON.parse(metadata.coverartid) : []
|
||||
const coverartId = coverarts[file.region] || coverarts.default
|
||||
const coverUrl = coverartId ? `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_cover_big/${coverartId}.webp` : "/public/images/coverart/nocoverart.png"
|
||||
%>
|
||||
<div class="col-md-auto row align-items-start searchresult">
|
||||
<div class="cover">
|
||||
<img class="coverart" src="<%= coverUrl %>" href="<%= file.path %>">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<p class="title"><a href="/info/<%=file.id %>"><%= title || file.filename %></a></p>
|
||||
<p class="info"><span class="infoitem badge badge-secondary"><%= __('search.released') %> <%= metadata.releasedate || file.date %></span>
|
||||
<span class="infoitem badge badge-secondary"><%= __('search.region') %> <%= file.region %> <%- flags.createFlag(file.region) %></span>
|
||||
<span class="infoitem badge badge-secondary"><%= __('search.platform') %> <%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %></span>
|
||||
<% if(metadata.genre){ %>
|
||||
<span class="infoitem badge badge-secondary"><%= __('search.genre') %> <%= metadata.genre.join(' / ') %></span>
|
||||
<% } %>
|
||||
</p>
|
||||
<% if(metadata.title) {%>
|
||||
<p class="description"><%= metadata.description || __('search.no_description') %></p>
|
||||
<% } else { %>
|
||||
<p class="description"><%= __('search.no_metadata') %></p>
|
||||
<% } %>
|
||||
<% if(metadata.title) {%>
|
||||
<p class="file"><%= __('search.filename') %> <%= file.filename %> | <%= __('search.size')%> <%= file.size %> | <%= __('search.upload_date')%> <%= file.date %></p>
|
||||
<% } %>
|
||||
<p class="group"><%= __('search.release_group') %> <%= file.group %></p>
|
||||
<p class="actions">
|
||||
<a class="btn btn-sm btn-secondary" href="/info/<%=file.id %>"><%= __('search.more_info') %></a>
|
||||
<a class="btn btn-sm btn-secondary" href="<%= file.path %>"><%= __('search.download') %></a>
|
||||
<% if (process.env.EMULATOR_ENABLED === 'true') { %>
|
||||
<% if (isEmulatorCompatible(file.category)) { %>
|
||||
<a class="btn btn-sm btn-secondary" href="/play/<%= file.id %>"><%= __('emulator.play')%></a></a> <% } else { %>
|
||||
<button class="btn btn-sm btn-secondary" disabled><%= __('emulator.not_available') %> <i class="bi bi-question-circle" data-toggle="tooltip" data-placement="right" title="<%= __('emulator.not_available_tooltip') %>"></i></button>
|
||||
<% }
|
||||
}%>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
72
views/public/css/info.css
Normal file
@@ -0,0 +1,72 @@
|
||||
.info {
|
||||
font-weight: bold;
|
||||
color: #f0a400;
|
||||
}
|
||||
.stars {
|
||||
color: #f0a400;
|
||||
}
|
||||
|
||||
.embed-responsive iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
}
|
||||
.embed-responsive {
|
||||
position: initial;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.slideshow-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.container {
|
||||
|
||||
background-color: none;
|
||||
margin-bottom: 50px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.coverart{
|
||||
object-fit: contain;
|
||||
height: auto;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.carousel-inner {
|
||||
height: 0;
|
||||
padding-bottom: 60%;
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
position: absolute !important; /* Bootstrap is insistent */
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.carousel-item img {
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.console {
|
||||
height: 1.6rem;
|
||||
}
|
||||
.text-platform{
|
||||
font-size: 1.5rem!important;
|
||||
}
|
||||
57
views/public/css/result.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.coverart{
|
||||
object-fit: contain;
|
||||
height: 200px;
|
||||
width: 150px;
|
||||
}
|
||||
.description{
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.searchresult {
|
||||
padding: 10px;
|
||||
}
|
||||
.searchresult div > p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.title {
|
||||
font-weight: bold;
|
||||
color: #f0a400;
|
||||
margin-bottom: 0!important;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
.title a {
|
||||
font-weight: bold;
|
||||
color: #f0a400;
|
||||
}
|
||||
.info {
|
||||
font-size: 1em;
|
||||
}
|
||||
.file {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.group {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.infoitem{
|
||||
margin-right: 0;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
height: 1.75em;
|
||||
vertical-align: middle;
|
||||
align-content: center;
|
||||
}
|
||||
.cover{
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.flag{
|
||||
height: 1.25em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.console {
|
||||
height: 1.25em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
42
views/public/css/resultsold.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.dt-orderable {
|
||||
position: absolute;
|
||||
|
||||
|
||||
}
|
||||
.dt-column-order {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
color: #ffffff20;
|
||||
line-height: 9px;
|
||||
font-size: 0.8em;
|
||||
right: 12px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.table thead > tr > th {
|
||||
position: relative;
|
||||
padding-right: 30px;
|
||||
}
|
||||
thead > tr > th.dt-orderable span.dt-column-order::before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
bottom: 50%;
|
||||
content: "▲";
|
||||
content: "▲"/"";
|
||||
}
|
||||
thead > tr > th.dt-orderable span.dt-column-order::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 50%;
|
||||
content: "▼";
|
||||
content: "▼"/"";
|
||||
}
|
||||
|
||||
thead > tr > th.dt-orderable span.dt-column-order.order-asc::before{
|
||||
opacity: 1;
|
||||
color: #ffffffff;
|
||||
}
|
||||
thead > tr > th.dt-orderable span.dt-column-order.order-desc::after{
|
||||
opacity: 1;
|
||||
color: #ffffffff;
|
||||
}
|
||||
@@ -144,3 +144,18 @@ td a {
|
||||
position: relative;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
.flag {
|
||||
object-fit: contain;
|
||||
}
|
||||
.console {
|
||||
object-fit: contain;
|
||||
}
|
||||
.spinner-border {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-right: 0.25em;
|
||||
vertical-align: sub;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
BIN
views/public/images/coverart/nocoverart.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
views/public/images/flags/Asia.png
Normal file
|
After Width: | Height: | Size: 448 B |
BIN
views/public/images/flags/Brazil.png
Normal file
|
After Width: | Height: | Size: 403 B |
BIN
views/public/images/flags/Europe.png
Normal file
|
After Width: | Height: | Size: 266 B |
BIN
views/public/images/flags/France.png
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
views/public/images/flags/Germany.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
views/public/images/flags/Italy.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
views/public/images/flags/Japan.png
Normal file
|
After Width: | Height: | Size: 216 B |
BIN
views/public/images/flags/Korea.png
Normal file
|
After Width: | Height: | Size: 486 B |
BIN
views/public/images/flags/Spain.png
Normal file
|
After Width: | Height: | Size: 450 B |
BIN
views/public/images/flags/UK.png
Normal file
|
After Width: | Height: | Size: 440 B |
BIN
views/public/images/flags/USA.png
Normal file
|
After Width: | Height: | Size: 224 B |
BIN
views/public/images/flags/World.png
Normal file
|
After Width: | Height: | Size: 385 B |
@@ -1,6 +1,9 @@
|
||||
$(document).ready(function() {
|
||||
// Make sure Bootstrap dropdown is properly initialized
|
||||
$('.dropdown-toggle').dropdown();
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
});
|
||||
|
||||
const aTags = document.querySelectorAll('a')
|
||||
|
||||
@@ -32,10 +32,12 @@
|
||||
if(typeof settings.fuzzy == 'undefined') {settings.fuzzy = defaults.fuzzy}
|
||||
if(typeof settings.prefix == 'undefined') {settings.prefix = defaults.prefix}
|
||||
if(typeof settings.hideNonGame == 'undefined') {settings.hideNonGame = defaults.hideNonGame}
|
||||
if(typeof settings.hideNonGame == 'undefined') {settings.useOldResults = defaults.useOldResults}
|
||||
document.getElementById('combineWith').checked = settings.combineWith ? true : false
|
||||
document.getElementById('fuzzy').value = settings.fuzzy
|
||||
document.getElementById('prefix').checked = settings.prefix
|
||||
document.getElementById('hideNonGame').checked = settings.hideNonGame
|
||||
document.getElementById('useOldResults').checked = settings.useOldResults
|
||||
}
|
||||
|
||||
function saveSettings(){
|
||||
@@ -50,14 +52,12 @@
|
||||
settings.fuzzy = parseFloat (document.getElementById('fuzzy').value)
|
||||
settings.prefix = document.getElementById('prefix').checked
|
||||
settings.hideNonGame = document.getElementById('hideNonGame').checked
|
||||
settings.useOldResults = document.getElementById('useOldResults').checked
|
||||
localStorage.setItem('settings', JSON.stringify(settings))
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
function loadSettings(){
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
if(!settingStore) {
|
||||
settings = structuredClone(defaults)
|
||||
settingStore = JSON.stringify(settings)
|
||||
|
||||
6
views/public/js/utility.js
Normal file
@@ -0,0 +1,6 @@
|
||||
function timeConverter(UNIX_timestamp){
|
||||
var timestamp = parseInt(UNIX_timestamp)
|
||||
var date = new Date(timestamp);
|
||||
var options = { hour12: false };
|
||||
return date.toLocaleString(options)
|
||||
}
|
||||
32
views/public/js/video.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// index.js
|
||||
const videos = [];
|
||||
const tag = document.createElement("script");
|
||||
const firstScriptTag = document.getElementsByTagName("script")[0];
|
||||
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
|
||||
// YouTube wants this function, don't rename it
|
||||
function onYouTubeIframeAPIReady() {
|
||||
const slides = Array.from(document.querySelectorAll(".carousel-item"));
|
||||
slides.forEach((slide, index) => {
|
||||
// does this slide have a video?
|
||||
const video = slide.querySelector(".video-player");
|
||||
if (video && video.dataset) {
|
||||
const player = createPlayer({
|
||||
id: video.id,
|
||||
videoId: video.dataset.videoId,
|
||||
});
|
||||
videos.push({ player, index });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createPlayer(playerInfo) {
|
||||
return new YT.Player(playerInfo.id, {
|
||||
videoId: playerInfo.videoId,
|
||||
playerVars: {
|
||||
showinfo: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||