From 3f0b904246ec4a44a4a7fface56cfccba20f6e7e Mon Sep 17 00:00:00 2001 From: Alexandra Date: Wed, 21 May 2025 20:34:28 -0600 Subject: [PATCH 01/46] Implement result partial, and create new results ejs --- .env | 2 +- server.js | 2 +- views/pages/results.ejs | 6 +- views/pages/resultsnew.ejs | 102 +++++++++++++++++++++++++++++ views/partials/result.ejs | 14 ++++ views/public/css/style.css | 68 +++++++++++++++++++ views/public/images/nocoverart.png | Bin 0 -> 10458 bytes views/public/images/testcover.webp | Bin 0 -> 10098 bytes 8 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 views/pages/resultsnew.ejs create mode 100644 views/partials/result.ejs create mode 100644 views/public/images/nocoverart.png create mode 100644 views/public/images/testcover.webp diff --git a/.env b/.env index 08cb143..20174f2 100644 --- a/.env +++ b/.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 diff --git a/server.js b/server.js index 77978aa..4eb3e73 100644 --- a/server.js +++ b/server.js @@ -208,7 +208,7 @@ app.get("/search", async function (req, res) { urlPrefix: urlPrefix, settings: settings, }; - let page = "results"; + let page = "resultsnew"; options = buildOptions(page, options); res.render(indexPage, options); }); diff --git a/views/pages/results.ejs b/views/pages/results.ejs index 35f37bd..74efa8e 100644 --- a/views/pages/results.ejs +++ b/views/pages/results.ejs @@ -47,7 +47,7 @@ - + @@ -152,7 +152,7 @@ + + + + + +
+
+
+
+ +
+<%= generateAsciiArt() %>
+                
+
+ + + + +
+
    +
    +

    + <%= __('search.found_plural', { count: results.items.length }) %> <%= __('search.in_seconds', { seconds: results.elapsed }) %>. + <%= indexing ? __('search.indexing') : "" %> + <% if (settings.hideNonGame) { %> + + <%= __('search.non_game_filter') %> + + + <% } %> +

    + + +
    +

    <%= __('search.displaying_results', { start: entryStart, end: entryEnd }) %>

    +
    + <% for (let x = entryStart; x < entryEnd; x++) { %> + <%- include("../partials/result", {result: results.items[x]}) %> + <% } %> +
    + <% + if(pageCount > 1) { + %> +
    +
    +
    + +
    +
    +
    + <% } %> +
    +
    \ No newline at end of file diff --git a/views/partials/result.ejs b/views/partials/result.ejs new file mode 100644 index 0000000..9be8684 --- /dev/null +++ b/views/partials/result.ejs @@ -0,0 +1,14 @@ +
    +
    + "> +
    +
    +

    <%= result.title || result.filename %>

    +

    Released: <%= result.releaseDate || result.date %> Region: <%= result.region %>

    +

    <%= result.description || "No description was found." %>

    + <% if(result.title) {%> +

    Filename: <%= result.filename %>

    + <% } %> +

    +

    +
    \ No newline at end of file diff --git a/views/public/css/style.css b/views/public/css/style.css index 136009f..0795531 100644 --- a/views/public/css/style.css +++ b/views/public/css/style.css @@ -144,3 +144,71 @@ td a { position: relative; z-index: 9999 !important; } +.dt-orderable { + position: absolute; + + +} +.dt-column-order { + position: absolute; + width: 12px; + opacity: 0.125; + 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-desc::before{ + opacity: 1; +} +thead > tr > th.dt-orderable span.dt-column-order.order-asc::after{ + opacity: 1; +} +.coverart{ + object-fit: contain; + height: 200px; + width: 150px; +} +.description{ + white-space: normal; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; +} +.searchresult { + padding: 10px; +} +.title { + font-weight: bold; + color: #f0a400; +} +.info { + font-size: 0.8em; +} +.file { + font-size: 0.8em; +} +.infoitem{ + margin-right: 1rem; +} \ No newline at end of file diff --git a/views/public/images/nocoverart.png b/views/public/images/nocoverart.png new file mode 100644 index 0000000000000000000000000000000000000000..865e8f85adac19d57bc38a2d414a9e7a17095c68 GIT binary patch literal 10458 zcmV<0C?(g4P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;wH)0002_L%V+f000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YC0WlPvNZug;03ZNKL_t(|+U%$2W2+53%AP9m0`OA+nALEa~1Q7&e0>cK3fj>MBk|=fz!-AN2 zVq|QM9||-HZ7YIEQ*2R9na%Dd*{^#}KFDs~K6me`RkcskGB6itkoVs6yt__4R;{WE z&FRw(0&cuQ>#s_DzV#Vy{y!xCAnc#owi1q^?K^m$_P^}DiF4LB-!=n(Tkq@E=k0A$ z`!<h_1Fg*<>x_2kEBofoY0-qpoLhc6^FL@*1;nC}DpFfWX1C%ri%FXz zvv0=qAz|(`p=y+UEm{TzCYIeNW$xNq)aFwJ!4#$zFLWSqnsDSEj4C0JRYH5gU@8*= zS(6K4^;^8#!yqVQGFChm$e-FA$%h0h7H^&TN;zr^u#3t(};b7qX)-lCC^ zF;yP_D*pg^oL9|xXEaPO`b)Wm^=nyuAL93so@~G$jajqmYh#Ki7_t5*CZev-ow={n`BzUi(DVsb)T_6v427tH_vQID{T~e@fFP7C*;= z9cfV<44TdhOIU@3+YT*LXo>hF3`r6)>%(DzK&rgfZVG%QkiM5y(^;SlEB0kKeL=x( zEdJ0=6BAvj6T(5J5qB#N*EV~k>C=3rlBkans#^GrfhbtO6WNEviyK)YtT2$$$)!yy zhl8Bkw=rnaK;{<(|IBLgK$tB}U>N)fLhi9vQY_Yh0b;Jtg8eYj)Ufj#vkV0dIJIb+ zs)Hmp1F|}t;gl9)&}=OU?ZL6E=^X|)IqZNjba4U%Xi_8|3Tp<(+4C{D9(ZChqFKTp} z#3Ju@o{y!Uc)-e9_>9N3JkD|;&5bQJ6HROdFDzlvO1UUC!<~}ybGcBzD=t+k$HzMF zT105kpgsx;PukOoaKlOI=mII3Ai*kNgs_!l_2KllplB6VI4y7n%n5G`itGU?0|8j> z{46GOY)m=_(200ndJOVWpEvB5GzFX7Wg<>X>W3BXI;+_{=FYnq7HTgP25IiN0MIj> zM>|$!7b>qN(opTjC?KqmFb=tm;R#YBc~JE7@ICH0S5si3rR#!Y-%4MsS^yb6V5PUx zhT(DrqmQN|_yICc+ha1Ug$MZrbhGk|-?}>S-d89a)K7#$Iw79Dz0F>pqi4LEY2C1;a=qyu4oavTzLK%Z#MPN4C(Ot=@6z_A zLXUJ>rX=$6q-(tRQ)&4<%%+dg46NUSLJj7{me4YVT|{}AYhap`c?HJPFQG)xAZS8M zv8$a~Ao)33s9q_)4O^+gYLaRIZs@|tQcT}&G}b7g5?eK*Ce&>41D8Cmh&)<}8I%eM zKseBB61odyAH&#GiYnq^5O|mawXpDjkK9*soj$I)H4z-#=F8s*ZTJC+CSM7hk6D`N zZ5*GofT@>)Q-i6B$#3U#!hy1A&r9+3*5}>QK2Z!pD)N4@yWA-#{@LJS29y)+JZ+Bb z+$lH~%$h_F9cZHfo&kzhRLFAOg?nj2K8x=`wyi}4KJ18Nfm%#RCkc9!07xwlQzbj-5PTiJ#g4C(s|GLNS}xJg;R?her5sp~aA^J(lYI8HXp~bt z(bZ=ZNVczT3QV2W<<3f_VQ{2cK-|6OwrLb8F)QuQupTVh3}n=o%MT67Et6Q{ zP+Q;EM3h%jLeKV~(i7cXrBHUO0fu+fhy8k^=bF3>048!Pp|vok&r%SP(t>#*-R=CI zhuGJ(K=Y0HBb{?>36-TLu8S0m)j#uSFF5Wk?F6$F6%F0i(m58oup}6U3Kx-WaLY%E zYz1@^%DGIbMPm}tmH?nsi$X!v<){xGN-ogA8)s(G-28ze)8vdo!1Y?YmhrCBbU? zWY&PMHv`9{@QgTPOVK?hljTfFrB`fU^bFKNE>KJrHpxMlm2X`tSYd>F>htTdG^!780A1BX+su?Se23J@^e z%wejT!FV%;QH&!tBj|0LkDJG78iWvrKHL5&L)d;lxLtd{!EFY=v;KeZcy(y75o)Yr z6Ia6qmclxI6xI`*uBt#ttRqEWZ<^8iEUY%C8%BG08oPQCaqSqB^(N@oZtbuMV(n zuG;2ysy~aF%>$V3&u)L;M?a}~m{$k^bzS4__6h*zFoHRZV6nG}H{%8tdwuX!V@IKB z7v<Az{jyror#ppSc!W5Zcst?ax@DZ)>tW{eV5| zqgWW-#QbQDjlke*MvTn^Vk)6^)N1|MF|@VA97@8ub>6YfJ=nW?7sfZI&__SJK-PuV z7PM^|SzjwB@Tp_v%xK#r1Hjz%fU(UUE{*|9k;U1FtiWz$?CX}s(m3eeW*(kY5dciA z@57#JcVe_2N5APO7dho5OWKTe;FT+%L!Ue>o!e;i8|n}jRw8R9xji1=iF2> zbwx{!)S&3Pk!>XAF=5DT&jbJyYqOYIxdXj=1by^P4ti=)SVvrn;=Dq{Mu=_GozjvK z!2Y^HGZnEo7LBk1Wa^52!Y1cqS*vO+V>y^k#9MzO>rv@I zZzP%1puP1P`}%!s?5%KhH1gP26KsN0lZnSuh<9nEv;g0ZW^1Ffl5;{annF2i4q)Ob>yuPBNDYLZBk~Hd5vD{V>VW}G`mOlvAUc`ObBMI4QUYO*tsOKma7ARxH^H+#c9;2&G0~O zaWtoX@cqch2u4RoF*-Vmk&zMfdOh@dJycbN5W+U@)ie$2x<$NKs@*4Earwzh_i zjSZ8aNY}#vGk#?Xo12>|9>qWo zlAvw9uc`_Y6BC%8p2psVS^!t(Mm`u)C` zGSVD)Ceq#d|J0QgTpynt7A(u~L`$=bS{KrAsX5(v644ej$&CG|u8(2j+?@L4S{5Uh z_nVrU!k#^QFg-oJEht^Snx?^ze)J=}_10T-)aQ*hw_a}@xd&?#6Ua8tx6ZM%Ovp-{ zBT8n=+CGu(x1KsiTpB~a-&aF?CfAo*q!0r3?%j*IxjBrEj_z(sCIsAZ#~qlOn!<$( z7qGUrre1GplsUGvgtdvuZkU*i-va_51z|#2f!BzExqZXsxNCX4nVA_JI&=sz#@iaX zjgODx`0?X7d-g1@UcI`zQR3xA;Lf{t{@7APWtmvzz@(zoM$tmF5Y@2_6HilwC`%*@REScI$B>*3h3V>ol>O!od5=tH{K5CW=|6<}g| zSYUD{2ukShQ!Jr=`33TQf(AROMoKdWV7Azylm9^+6`6CIJXSQnA&#>2H3M_&yP*G z4jw#+^XJct_r;`rvaE}1SNV|K(}2|w)kU_AqD5G8P)F|C`qyi00qsm=#IhCIuk+{6 z<6ZB1mnr~jYin3uUdHnBGFDetvA({J&CN|zRfUm}5ll}{V{UHFtN~+;n4O))rAwEx z*j$ccqea62fa}+}ZY`S0(}YJdt%D3wwp>h#`Yb@{P&B*NzbzW$(xppSSy{pK^z_ho zo12?hSXjXP{Ja{$UA}x7=gytOzJ2@fj(5Dn1YgX~&f?8C-!wfaw7IO;?dcm!GT1;V z&Jz9DK31%6C0)lLN?nQ{X{^n&XV2n2?|IMm`>R*4;{5sZxOnlR8SZ%Nt+(*fOE2NX zi4!!?Iz2tD-mfJ>9NM<-0ROSV4^mX9rI9QqFsI3KkVNW-E)KsluQ%R!1ILdaM+gD0 zz4jXB=jUDL+uDA4c^UKb^EiC?Fty*<*x2^{N~Tas1xdeEj~zq}ltpGrjxjo;N!RvS zZP*kl7MAQ zZe-XBT0-11t2wn_e;^{v{opysfypPfg=Beo*~IqLw8vJ;hQ~7d6g64Hty1?q!Z^G@ zH}m!cVU3zL>9YS+x|}`?nL>?joYN{6in4Y*Eh-({hhaqw3f18KdK`E0ers?&20)q> zX54HpS7_l+wL7=KsbvQ_Ly}-IJah(7WT6ZX%#n&RbFYn!jigW6DGgfVCrMoQ)P5G| zKqC5S0fdHAagEGER%897y$&8cNN>Bcva-AP+c%mJOPyZMJms8sYxfq-Z00{p_ahi| zW+lf|LZ4ZOpN%?M7U$u^hcP=lORxLl;^L0HsHKqARIZlun?U+nB~!I>-DVoIz%Zsb z--Le4nPq$-X3<&rAc1`H3HOs9clqJLYkq!bO~fLkh(*{G|1ys@z{Jo@OPJE+OpYk7Hj8+F%c*DOayV z1XsWX4|obybLo|vw->D>!IbGspF`@5SBcKMgdKUJ!;)yQz3)Bu+=E9Rc?4r)W2WOQ zEG*#r-~YZ@h^!UVE@8+&VZ8Uf?==hQ+S(dU zpFVwi?$a}PyN29K2;kWXgE*E3dGK(8p<`S^V|fXFj6YS9ha4Ll!^x8;ap=$?^Ez*C zZsLU(Uci+rSLkc-bkxZTmPADf$d_`lzFDS;Z1j$H8*Xo{HF(Vno0Nq1=FAu$Oghij zcQZ3HICbiji+x(xHBO&CjdSPDWv@d@RWX7$w4EhY(s$rpWD!KPbmL2D&UEC5mT<`$ zYzxO?^K)9xj47*P+7!*r&Ed%>pTyMElv$`YHa775^UveeS6@}NuctRxHX1g<0;X~> z2>Z5itX!W_u3E>^J}2MKuwDJE{m@J_ms>za8*G`sQ>RY3gzDP0Yxwe)zl;kPE~rAR zJx~&cm4q$4!?-QeBZ%#z#Db{R^b_M#J^uLPF7r1(KaVec z=}SWl3breANt4{|Bs2v7g>54QazCVnG}RpGv_wazx$DSXQ|@8N(y2W;KF?&W*16h!nxn4qn8{a~ zzm=QtzEfkFQWpjOi4<8LIBQGxb^sGal{7$X4GdS-gdo!gv zB5(Sn$yP!54AAoiW4SeluU)xcKxLc8|Iof5K z&@wYdf%%TTkO|2v!N@S9@K*emxt>G7MNINJ*JIUfsdY3?;W?h8S;a ztfE;gsz%a>1*H~=NxNvQP}U!_F5u?o=5`uN6CCSsCXLx4Z5KkwCzb(Qc^8JG%E}-O z(KdoC2wE(cvI(l`%qm zmBqnCvO{Jtm$orRf>P9{sg9V|G}dXIxo9yr)qUqV2iXnMLVB>ClBi75nvCFBwG|T~ zs>heQ%NTtr6sU{H(_KnZH?p9;9jBcB#J-rSVggk!#Q>u2^8{2wk z%UB{WEiz2GD3xRJenPpe1dO&{Y|WlUjPP@qJJ6@1C)6dJ!%W3f#V7Mh>oE z_OlACWz26Pvb)$`p{5nXMB7dM~)o9kt0X&{`bEhpZUyZ@QrVLgTkGxg6^U531d9bqQ`=eht%jWUx5MkBAN?p!o;;~;H#0MXkAM8*SXfxV4}S0i zw-9s%lME%H71JkaB}+n10-ItUYhq!9oKQ*m#gG)Nl8LCLuaS`vN~pG8qobpE>Zzyj ziBEij_NTnCimCXPNFaNFB1>qN07yNWh>acc?q6z-4)Y_jorD<&`_V@q#pL9qX~FNi z@4h7amF*+yi9)pwJQ6T!wJ#okDeTl(Ilm}%=!yV9B?~+ZZAks1n4Fo0HA#NY*0%TD zbB|e*OioT>|Ni|$t%jpmv8fDJ(4t=ZS1ABK<5vX%C)-I)W{Xg@ zC{cW>6YnP`CR}D}Zflh5@@DVW^jUlAZ_O3lhY1v1bQx9xwgUAyKItc8Vz9qbIY#NQLKYTNJGbC)^tgP?^ho_EH1*L)+^ zf6~lly4YghkwaKKKi^GMJ0MmYV=;Yg zl@aAwfH`bV2zf2G|M)Sy@{ivv{^31Epj)r!pMM@7``E`!y&Asvz3-WzlHD5Pvh~`( z|G4OgNDjQDtt1Bs`KVBe6)Xv87Wo+d?!fV5Lkr}Ip4c2)>KNCrU&j}}_(eSR)Kk=c ztE;Q{^rt^f4_qqgdBs942M-)K=RR0f03!sE!Lh7BY2`pbZhe1nK(O3>p#YkuoLih{ zpM4e+6BBsii6_Jv(w8q^#xu`6gYSO#yHwtfxpq&kvTbVbK6pI48W!)_!d=NZKT$uN zMnSgXw!`}zJpLbW=Y#j*;+Y>%z6FbSvAELUF+cgqPvWaz{VG24k&obk2OhxO+#F(z zSX*1e#fum5?Qee@pZe6NaOKJsIMH;#W`MZ~xzaqn%hbLG7V2zxd*dc=5#-(>Rf0c9}$TD(dy*K?nc;z7xNS zDg^Y1kSjRkztr*AxgD}$1x<^G$0_ho$ckN=p?`Dwj6S;s!-16d+f7WQC-Q4`XwV?6zv zpTsY`?+FUvwE9>UuoQVy2qHfSr=kQWj1BNpANU|1`<>s#SO3SKXTxeHeZxBHO!YJq zJTO57d)(ALrv&R)PX1>+^iv-cQ~Yg!)Y8^POW#OcW+U%-r!L0=^efxn%x{fNOyZGW z`!L>m{Q~~)bAL@Q5Zjs96PZ~CZEAvJY04`_p+En#AI1kB`7kEOCPfB91G(nP$_-9w zzzmLMY5d_$LJP-DjM_MG&r$sHZ~PWE*4FW@FFjY3bF)+;7k4ZM=)|};Vr`lRKX>9u zJn_rFg}V@^x_Eq&tLto+X@xj z4*0=)50Cu(f5fR@`EA^L?A@Iwk)ilbY$3%>=lzF`-cq|nQD*jMW&i`Yc=jCr_uu>_ zo_qGcce>EtqSD)OzTE5ApZr67@X=q#p@T;$tx^@Di&7Av0PYv-Yp>y3r=Q3F{I7pPhw+-QNgZ5RLD6!9B}K1K|K@+e z`%aw1p}US3*8o<9rp}_-ZVFaqyhPY@=$dv)_*ps4cJcf<{Oh;AfxrLj&*NYI=^xZZ zW~!!HEt5+cd%ny0S?_)K2XOL(AHjRx^>euU;L)PRmb9AWDVHSeZu(enG^!y!I+yfAtmo@ZY|VAHH%1ul(~ji^6{kbiqIw(sAy;?|nFS z^qn|*dlf*S=6IcJyweXOx}*9{cd^G1$6YAsd``hcEPL;LmuLD<$i zja@Obo`FO=AQNHx-6*r_vYT0XSxmMEX$PHVnxHR&)FV^c&Z5NteT1!N;+1u}*3I`U zItbXfQ~=*jSZX^2QVN*5c_7Qjss!+!K;}y)muJjK_Ve7FVyOwfq|x9Iu-ttXm?D5D zFrfnz$avT6wa3mH+ImL9N@m5YO~{$Wran?29t5RD5C{{*mb2vk^$hweWs>I38ReoL zvOdFNLNpC#@ySYrh26$#a%=v{uVA04<|QhX1_FSsKrq^Gqe>@`8+10E)1=Mnjs%Dt zNh_NAj=&nAtm&DxQMxnJD>fO8HU%d{eYL?)%AxC){+TfPVwO3_DSr$4F-y&d#Cx*9 z(U5>k1BazDXk5j4CrA|5sE-ZRQJ~oX`&<|pgitweXly#)>JwSbP$=}XPyl~y9iJ?w z5U{CQ5e!B%)i+P!o${&8*JA{T(YOhXS(381qS}n91$cgXNnS-3%!cjgM_@yqD9aMV zOwJqt00z|?WJhP++;|*hT{WQI*9u4JOiZ zhMCFksZj`Qq~=YzD+*!?b6q;Pb_x1`_EXHfB+DZT1R;6iGv96EB0no7*cn2IMZPfNk=SWa_MxmXb(O z%c)c*Ewyujho^sOL#n-6NMjFpKu(tJP&<+I#@scQwDq z3JHTXFxef$|ZiK$f)gOm2#jYZR0hqwfmt_g_@aS zT?rHsK=1rzVG(#hOc*s%N~mGhR8Y-DbT2Zq=y3Q(LZoJP=!IiQ1p;0y2m*L&al&Eb zQKj~?Gf|nm7{FpbQLL-8ls(l(LDC$0Px^=XJ&fif6!|IXMfPeGpqm7CUa0BJ)Y?#U#)`SyW^CYZL+o1u5@>_yx2d5VFT!)-Tv^3CYNI$DBSWuCAJ2Kuy zB3dI95@y>&w6nqqmTvgosoPd0UY@PfWbhv7zJ%guMal{GI?*gmn9)R@006_Z;Yvug zco(e8TpiPnr8LFV9d4A5mZFGAGYRRYBcap!HNX)qf-5qxrc$iMoQ!75nM!M3QA=3c z5ayb6+K2s+NDTp_2(fdTiVg0NsWfa! zSxcy_Nm&a3nj6BGI|jpIV0{+$yn zElnb|eI*g5Pa?Tol%T06sm$feoM zi#-V~X-tjsVo4JzWI`?4Dlvlrdehfjj!$x`+1Vdn0W0s(1lZ}il8@ynW~kxE)2y5? zk~TbyEcT74+PxF3UD@hw8wUHDXBoQjs&r4 z7zpRo8h>!KVXbn;6Pa1DBD9=$feGNs@s)C^+Gej{#DP=;SSjYKR69wURBOP=Kqowd zQc9CpOR=E%K^`27g(furFy)Lx4iG#Q!*G~D1_ifjBe9(MfXJ%_G%x?Y#0yTQ(FEGHBlT*xjL)KOFd-GP6u6!ygVb&yN(!Huu_9A7;rT& zND_WzX;#k&lV$>Sp^Iw8kGBfChT3w`f+dyCi_s8nD|fs?H_=|c`j{;M@DSxZD7a0l zMOSxWtzbjgE;L+}rzc$ftWbux#Ry{;!bkJ^Er86 z1%q(Q_)#k%Hfa#-?eQzT{Fbb95vq zB^{6X7C}HaG!SN}k=LIo8L0-y#S|q%3z_{W(MGJo;!PZD6+D)1G4^i5HL01}3 z$~)OgYLsD}6SMOkqiB+)Kmo&LQd+TSmyJ0p*tqIlf}Z(qE{$n=I{@`9RR2H2p9V8VYuM1IWN;KnGV4EV`vRQ&<@cPxum6HT$^ Q7ytkO07*qoM6N<$f@B>ZegFUf literal 0 HcmV?d00001 diff --git a/views/public/images/testcover.webp b/views/public/images/testcover.webp new file mode 100644 index 0000000000000000000000000000000000000000..c2b3f5fbc0e8764464f75985ba00cc6ca1c678f7 GIT binary patch literal 10098 zcmbVyRZv~cw(i0$xCIOD?(Xg`L4z$Mz(N)-3->^9hv4q+5Znpw9^9Q^H~&6+pL-up zorl|1vu5|`@eS*5Jk08;Aul6iOA7$#N=vBesPX9`0|0;ze;))WfC&`fuY_!p5&*z( z17txlAVPhC5;0{-kfox?&81lhU-JKeWbUx-Dgq#A_wB5|EMNC*zZSL5ZoGWINB1<+ zbrnrqWP$v8yE)5$X?_vC$Uf_Cg5=*rzuFv0@8jPu&PV++x!qI)9p;>hzCY`GRYGFl z^G`jWbC0H;>n{&pA#cU!kS4s!#L7qYX9%2+xi|0q`x@lZZ;7w{BuafV?RD<8`VlmA zk?}tCFm!dJ7Hj>;ei{TY$$2+@Z{Bvl6)o8`?LPGOhp@giz9Y<6KXiSB_`F-4BD@Se z7r(yW<|Kl!hzvbMUK)p|iwm`P;4(~OeA@7^(i}yqCPe|(Ou6314f(#5S zP1>blmeSG-qm68*B_(akIz=Q&xcCMPbA5Grc_)_oDPf|OSh6-cMT82eLjslS$amEr z4#u5TPh8c>`BJPr?Xn#I8qr#dIWIdLcbCs$kS~^n;crw${fZ5fW4;WgdG*T&KhU3p zV;TsvNTN|ZEs8Ftg$iF|pqV|oJcONN{X9$8`^7h32HjV9u2)~`JeMi)-+$||f(LX3 z-W4)N77Pz%;d@%dD~PFZvM$p{2#;(l(Zra1Dl`~xVqnbCs3bg$K48~hm9$Y)SBs-CEkL|v(l?>-a)`2f=7^-&p(TyqdIHW2j27A%a zR0`k0NRq;mbBbT-RE=Kn*QfWNUgF#k@;mXu$^SqUtW28)gofAfx72D6P^q@}1fw#< z+xhML?~Hs|H$S|vMCZ5R0^a`)7W7GJp-!~w*PIh$hdDN64*MU&Z>#L8W349@!AppMe zs%Qnu`LqHko11Nt&SVD={L+m;rSZgX&O^lLqx(l`#JC@#z*)BX^HO}o>)Dc~V{aLZ zbCC+J>b4kVAd64oE_KuM4_AP5SK`Wohg%6X4?&J(1*|Jx;qb# zJxSx+Q2!7vBcp?Nh3a3NEBsAJdGv4A6`I}sZ`8!w0=yj%bieP8zAE~m`**+HDsUrB z|Ltx70IE;0e4sdC`FKkn-61?`r$y_@kcY@2YE%u5q)DUVJ=!i6!AXv>Ph-HvjV zafRHn?6-Ao3@rj!HkzS+F5e8fZ|IQGvXV@ef0 zp%K4qU0v>Bxr=}9$M_TJ!<({TvhTE~4fy!RQESPrqda;s+U(G?HNrUcD=NT38?xMt zU!pI6D4=2LKIGGCrxte-z>_mw!37{CFL{G<*k~~(j|jh$ZDp*GbzfRqsEm&{KL@j% z50R>6QlG;axL8r-{;Cc%;<#L0V{fwe_|h$`Z#4`rm+{@3G50v!TX}Dg!0~N(*N1*o zJyDYH1w)_Th6k+b+CH?ss#`#m>wWe?>T?&Qi4)0C73){;wl`NwZ*!56*|Qj&a;wi4 zoC>D?&px763 zKD8M8y7Kd!;A4UIvVcb;@d>V)a^0y$D3GMO=o6Fgma8@gg^g|6%y(IPYV`I6mYV@6GGw`m%LkKxh+yL$8wTbgw4$4MC2p$X0V z0)#xbvlQeT`9oXEJs~RyND#&Odr3#tg$?MTFeI3cZ4jATtm0I_Rx~IP0^lR9C@f&M zKWDm;30Trb-S`Y)d`cQm+->n3y>L-r4gS~?CqBrYEivlnygRen(`4qVLvQUXFq$dV z-7{LsSBsNfu1UmX0-d%fgxYau)e4;%CZ+A)Un-MZo}0ZJL*G*9Tw4YaYB`Rz1gsZ1 z<;lsS=($>llVoD?zW79)mlJ8GW;0=6pm4}$0v}o7Cd>0W^xWjH9(7PL;V(khe`y@z z0Ys7(ch6|#yB}>^$9#mokokzds%@017?@N_3q*f>XVOyEPAiu>`evBu+kyp=H&1D2 z=76q7yyd@T?l?Emt)IGvgLj0z$2|W$Z6%z@h4$$CfkP2X>bc&vZU+0LX5>o#0 zV^(@y5gB*`yRIiGqcN!G_c~4)o#-U@5&gQ!;Gz674dJHMWPSs(B+INMp@k}cJTu1~ z&SbX+LWtrc0YSK8sbo5O8n1a8N3UYbFu4wLY$0}fRrlJ@o)?Y7#A@$23$DGm zF2GbGvJh4PLnMrI&N!N+YCGA}dd)s(l2=bK>tlbJ(x%QAtVw8M5zNVAx##W{)X=y+ z3p(#7vx|uazmQKC%B3USco!c%%#r#U%`-3p96Qi%E0UnJ6ulp065nWQf-{KAm}IA3 z*}Kjj$l80$6P5~Gc6~97jvCst7^xm~Fcv0|l`Q`=IVMod7 z#&e^ExUX%}QH*0yUC`T%bPt-(A2K^BCIr}xwLNsGm_KX_=Xxmy|MY-8-1e$yK7@-e zZ|rDWM<)Ij;u3^P=_p$goz0v_*Xtmb!vAYjz*fJCk;TDWNeSj~f3ckBh;R^rXFe%* zwGgG5GmPjw?$0g3gpwA)v|*SOHBykIq?FGC=*6x+5|dhh2OhvEcYuDyli15KxwvK5 zP6ZCdaMr}L)bakTD}-}J3I3b*OZldY3p-nDg%?F5#W2GlfVB!#37#9#de zC$`ohB8v(ehSYUdIX4q++O}&UHtx|j_W)pr4Xr;OeD~`9?XDw3f%ZZQ)0E0_9MfMN z{ik4#D*4<1@YW%yJBpUUcqn^M5mhda7s*hG88Z`;R;Z66C`rdCGAVGu*4utFlj?Z= zP@2J)WbA)U&`c=uq32?=#b=OB{OV>V-quo}*>d~$0yEzHW6HYg=2C8c%&d0S1Z(cs zU3|W|Uu>~&`o=`yu&#kZ62A9DSo!F!HB9+6`k0kVj4?*b5FzLZ!)p)Cb%}#OuW#nk zy7R3zn{KAk4`QvU>gj{U?epY5i;hc@FuvU_Q4mRz67q>29*5!!1-%zeCwfx;uyw`^ zrj*(})(DbW`p$?kDk>THHGxPRe{1ON zgr1vn-Hh}M(zN3L{At)K9jBE<{Q5_4pLMkUi*NofS*=4n6zH@ATpFAk@%lrlvXIET z*dN67)X1utR_36+(f0)6A*tB(jUR7)1(sPMpW^9}l7oAXs#?rnJo@u`emp{VDChL7 zW`OeG;>S=na>A1RsNEG%6uz9g?h_>XR5qCxS%9q;qx&H+xv z&FjfcnUqjBUcW z*nV`H1PQ@-q}9<1j?A<=ZlXj+T{s9~Zm$ZDmWs1NGb;!;H@l^^KN0gJlb3?F>>BbJt+D9VMK2>ZktIf z>X(rjV#f6XzDJD+k=}9hwCP(GrLU;4$`NF)(-l%0M;=uDyJRfD503kv- zq%Jw$cyVypZSid+{f~7l6UFw(?9?j@^(fwVl7&aFW!+*ga~kDKG_@xOwiM2sd@lr3 z3q%va4RB`Gd~(4|U)ke!RL@Any#0EY^4rcQk580MP`@k7Sx}u=9dxKa4?H4Iv!u>v z46EEX7N`%ke~%tPgUXreExL3`(-<$Uhh9r_-}yaE=reaKp_7#3M2q$SOPn#Ph{Ll; z^F>a2E=Zrxdt`^n*VH18v-)rh-5Rq4zh?f#fj6Zg#6owmQdT$7v#OHYM)8akfWjGkvH_sWj^*%UK6iY7b*FCEh(urlXg$6Q z&VU=@A(JP5YC<3RRoI30)$@o=?aXI5u zaW43z8w4{w*E0`7o=bQ)>2is?BE@}{{)YEILJKY83AB26V?vgR2Qg=lH!Ix}ugK2CjKr^$(tVHXtmxDM@id=7my z-G*GVf9JeUy+NEwrRIwNQLwZ0_0}da&y7p*N_X1_WqBP*j1Di`q)nc_;uJjG?u&m9 z+d`{2z0lZ|hYZOfD7ndjUWVO(Wlyek)5YdRqv>RCmI$uI5m+RqgZ#@@v1?6;6;8S6|NvQ0PAZkU#=DL z@an+C45wffuQCTsQTbOVAr#wvtD8~NSRTO0n1I=`@S>2J*XuyliXq>Sk$koCj*k_M z4fQCzK@Ua`u=Sy3O>f$unS^>-$Nf4h z9xoqQ`F;3S7-TGD-l0Y=b&kv>TO93PYVFqk8IlK_M%UxxFXj1Rr_dOJu6J{laKkFY zV$k9E@)V|Xs(s#MUt+v(Y?dRLkVmB_kLIj04;R~QF8C7Y9}z{31^ZdIOzR{*)8^cL z@%3` zLR_Tw0-6+N)R#Kd&+bInVd$~gyBw;ODby7o|4o0Uw&1&xxPxhYwNO!*gn}MmY2_nE zN}(!J!bNxHfV`b(^J6+1{gQAnh}4Ae1fKP7ean!u7CZ#49)YxtEXc{3*YMm9ra~-9 zX5mzW(4a7KDB)9c-qA!|U507iU%VpFH$dVl>>@)B$DMT>occUmOXvPhV zy%>!{iDE(}MhR~lXa^YjOw;vlwWQ+L@WC2qc56fr=-&D>-ARFna^8QEtqYac5#@vo zD@|pS4n2FXLHVY2bD^(jBm7v=+Z6m2@RLdT-K#oZxdp?C($OqW@iPCY8r6YtpK=GM z@&*C&b%-u4NYBTOw6XU3obivGvY^<>?@U5|Hw0ora{O}xiD!hIIJOnV8ugn&G)kgp z$*RE?T5r|{mB*;9SrR>#MGX-)Hmg1g>0>;`09mO`PImkH3F$u;F5QmR3=x!yRhprmD@!0hO?(8D4JDa%_XF*e}IH28=&F{if;Wvd_ZVH zD6c^O5a=hWbrzZi_QL0ehx7um%pbDgu-z4&YJWefJEY^pX^4WbUn70g{?@cRiMxeTH;b~)>_+rGf4Oi zzB1G_LmJUe%PMPMYIQ{1SJJ=ogehed_gD zxT~g3(~S_++wl2RvLD)})^7Eq3`iXu0Z%oNCUgeDjyiBco`g{#g#GI_R$D~tx- zXqM_Um; zJkp7?ZiA_Whxc-4HUf7|$cFn#^X!siuQ+A0TtBl+glXf%Aek53jGK_5#2JTo2kxD* zmr_&nCFp%{XD=N)*O-13u?hcLPHHVhJG+hjez-p zI-@s|TWdcsZL@x|aMd$nJA=WNDB0vu92%lUgKW z$DP}22+^(b%FM<~%veRj9tY-~cq1dGVTpX6v2T;lj;9lz%v%y8Y>z<(BwjbOqvlHV zfm68iV|g@u6t7D7Kt}2o@vlLmjmUh}ip!k3id+LnZ42q@RGbF<)CLUjl(XvWE;w$mx4KY{?auUs*RdAzdOS6J0dbKoG`u8DL%@IJ`lu>a0zVt z#D=PI^crCn1bg-YrDRY{_gX?KL*{Mypwc*E?ZaMQId!~?rMuB_d4VKeN?UZFU(5Tt z?`u5A>JJae!hx$B4og6U+zkWgI=OHv`RK0%WjH7-#wt7U$AE8AhJ{Val}7oW3RkBy zl|FwPkD{x{K6yruN1n&JE=AB{ z*~ujMjX$vr!#Y8&!Jj?*w8VcwL$qQMVHk(4WO-;Oubs8eIMS}WFxVaUMQE*9mpPq; z-qal!k!@sK{l*-U&mlnn!?6#skA-v2{l~gf1(&==A-Y7p5nD_H%vlXNn?U@6;s;P@ z$MQUIW}$b(j0@ddrIu73jumL@!7_7E9c3Pt>wtI1E;7R)NYWKKE~-Kpm?T_C!mSD) zl>M_l4MDLpX0cIhht5qP?x7+*;@~r^vg157*Kg5O<~Hn|;KHfQw3~}Ppae8t3nSf> z7glbj$1fgdnd08zxWy>k>xSP&!x^TtGr5KyJI&!l{d5>(VEq`1F`lCC*`I)Xmm~%) zA`3L*S+FK9+qLKER|g1FWEXUf^`e?IJ1J1a)q>8`cpy9gag={#a5BGcqUTrM=^J+q z`#TP2qRqLG=lw@g9J$kc(jDbQTj9XML0$K-3tV`$LGLZAC3)}sg-5``V(75VU14EF zho5>9V8Gfu(-c>F zAU+(lZ#j!yJdDJTIC?J9`_txWalg*o-RteagPUmw!pynQgA5!?D@yxvN=&JNCUUxt z!!Gw4lOwP?n|AjpBmCX0SE@|H9|fgqB%~D&^`mfw-+p;-ui$h_wP?vJlSINsjk|EV z6YO>6qYg$pSy;G-^WV*jLCd3k?hR0W#fBHBaq31@m$l2U`?2xfesSMO^X?j@YyMz+ zpnnC@n+6UKtErJ`ihXH{9@^h=Q><9-x&FEqds!%i^CyOhhSThdaQgHqeIjPbaM+VP zOK*pvJqPUd!PFdIc1Ot4f(qJ28pcnSK&+VmgCDVv?XwbN7Z4p#zJ#lP8{_59TLD4( ztStGE5*FDLenZSy7|@>tAFkq9%flxfvV;(HFo!~oRs#M^UY1W0oM17ybsXc+dHyI6 z`5auXMs7&5Bg(vEM3pD<1?O^q8Tk&#s&HT+3KDk z8*y3A=Nf8APzH5V7-tHpo4@RO^k%cDLeJBoP+(6_UeOE(m%VwBk*06L3yW9@^SO5N z55pmUe6l={^iszRa2vwb@OaHS+!>r8VQey~ynB@`NNtemaw_|F&mJ)K#OJ_|`Uo0r zVjYz|VW#aih=IcSIUM?cH1Ad%8>4i*rk#iU?(b}3eTm~4K-Xziq!GjDvw7t7&Nx-f z?Z_%tl5VGw&Xw9?Aa=R}>kuM(rjaWl3(ZL{2)-6I zIyOJYz7urqyaWzd*9Nf1;=>&TjIZAk)#27rMM$wye(+d5DzHJK4wj~QfZ7(im$2bI z-cq0rBCgi%G|eZIh|;hud&kyG{rO2pXQ!+6W4G$MXnKvMtPjU`B|7!TL>tKXA)eV+ z<(yk2DHs8dr(5~Bk`uwD5gf3ZY zbXu`)4lT0XRWVxgsEO2f<@BncTn=BXx$6C92a@F*>tzd$^Cp}6g;J?7&p>KgUKTb@ zpYGydtZNhRY`8VcQNGdV@+=^X^jS7);v_Q|hLX0+Yn3QGj?4`3IRWYl=b#<%zm$5= zG01=dF*Xcj5XiSxwgeqMo?NKE0BN#Bw>a)R0jvx>_|a7gk1XujI{Ph~QDmPRd( z8^TWZTq^cdM(#;jLdSY&-yzRmKYhyYy>uJkp8QB2#*3HVT^E2`hfDqA*FzY^5W?lw zf*Ke*l90_NCN+63WH&@P1Ka?Iv_aW_n~9t z8sjd@%d&Y0{*MpRrAt7IOYl@S;Crqce5*p>$w&UVR!kG&P?GTrwRt$XM7Mz^RRF(U zq6hrkVdP|yH+8}&CE2YA5i`sOx^;k*hHe)(Ev8yw0Y_GaA}Z<=vh*s(E3P--b9+Pf zp~XXHVP{r}9|{cqF*2;25&l4wXr=!zpe}z0+m4Dy3owWX?@l2>FuZT8z3gI+N9~!j zu0@*QEnQ``WS>d#q!Eq|1`q?&yx0nDoJQJY_T)R85my33z7SB(R6bdVT&fOL*1Qju z|E9HMB7A^U2I~BCx|=FV120F)XgfK$l9r<`QOZ=Q9CqOu)VaJ5GqWBnPjRqU2%qcK>pqk@a%al0R!6FAFF^lXDt>`lRxunZ=hq^?b$5mkB*9cX@#XMq zJ@7q2d^%cE)KbBHVyT!1HpZ8*g!ddK688oKE7UYW!DHa^jR~rCt(7xJo2@ROO2R{K zU+m)ddyB$!y|5;yq&U%@|A#x`5VXVje8WsQwI#J(Blp)uM0m6}ml(O%AGl@J1`C;~ z^I{}03~|4R2bs|)uUtDaCtzNw&Sy;skDrvF>?ybYw8iUKsZ@K#mxpyJ7){*=p>hQm z|EbUYX4SuI?VXQ4aIXVjRf?FS;c9_GxTo7S{5xT2EdQMDrfxEFM}$@$(!ah{DUO`d zgLd++*V_G)^99Es5?8Mo?hlEThnR0q-)E|VVWC`_p^!9;zjy!Adh#;=yyfK?0Eqwm z4Dj!vr>sV13D+hZ=W##`i?BK%kuZ&pC zLH4X3rjD%aENray_WvaIFKrjFhUNbaAMnKis=I^PP+DU=TUF|I$!17YU6n}SEENmDYnfc9uKxQB(7Z)?1 znJFhTJG+G`H=mgq50KyNKX(2nyg0wK43{`B4;z;>A3M9WB)>S16hF7L1Sbc-1dkNA z#DB2z4lZC*2Xo8+__q1$``=jZ{}n4B>1=5V203eiKz9G3ftoc4405psIg)X3uyB*n z>DoA0fZSc^|2d=oN?FR%*~ZP%LdF?nPxh}O3)uV*0r0T#@|s(kaWZoPd3l*F`ME5a z&Gpx8Sf0*+h(O)|LGyU&I_`CS;R Date: Thu, 22 May 2025 11:07:13 -0600 Subject: [PATCH 02/46] incremental commit so I (hopefully) don't go saving API keys in the env file. --- .env | 4 + lib/database.js | 4 + lib/metadataworker.js | 3 + lib/models/file.js | 6 +- lib/models/index.js | 2 +- lib/models/metadata.js | 55 +++++++++ package-lock.json | 110 +++++++++++++++--- package.json | 2 +- views/partials/result.ejs | 2 +- .../images/{ => coverart}/nocoverart.png | Bin .../images/{ => coverart}/testcover.webp | Bin 11 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 lib/metadataworker.js create mode 100644 lib/models/metadata.js rename views/public/images/{ => coverart}/nocoverart.png (100%) rename views/public/images/{ => coverart}/testcover.webp (100%) diff --git a/.env b/.env index 20174f2..ac3e62c 100644 --- a/.env +++ b/.env @@ -24,3 +24,7 @@ POSTGRES_PASSWORD=development # Elasticsearch Configuration ELASTICSEARCH_URL=http://localhost:9200 + +#IGDB Connection Configuration +TWITCH_CLIENT_ID= +TWITCH_APP_ACCESS_TOKEN= \ No newline at end of file diff --git a/lib/database.js b/lib/database.js index 13b78a4..768e588 100644 --- a/lib/database.js +++ b/lib/database.js @@ -4,6 +4,7 @@ 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', @@ -15,6 +16,9 @@ const sequelize = new Sequelize(process.env.POSTGRES_DB, process.env.POSTGRES_US // Initialize models export const File = defineFile(sequelize); export const QueryCount = defineQueryCount(sequelize); +export const Metadata = defineMetadata(sequelize) +Metadata.hasMany(File) +File.belongsTo(Metadata) export async function initDB() { try { diff --git a/lib/metadataworker.js b/lib/metadataworker.js new file mode 100644 index 0000000..77e70c5 --- /dev/null +++ b/lib/metadataworker.js @@ -0,0 +1,3 @@ +import igdb from 'igdb-api-node'; + +function getMetadata() \ No newline at end of file diff --git a/lib/models/file.js b/lib/models/file.js index abccbaf..b3f0467 100644 --- a/lib/models/file.js +++ b/lib/models/file.js @@ -50,6 +50,11 @@ export default function (sequelize) { }, group: { type: DataTypes.TEXT + }, + blockmetadata: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false } }, { indexes: [ @@ -59,6 +64,5 @@ export default function (sequelize) { { fields: ['region'] } ] }); - return File; } \ No newline at end of file diff --git a/lib/models/index.js b/lib/models/index.js index 0a7fc26..6a31023 100644 --- a/lib/models/index.js +++ b/lib/models/index.js @@ -1 +1 @@ -export { File, QueryCount } from '../database.js'; \ No newline at end of file +export { File, QueryCount, Metadata } from '../database.js'; \ No newline at end of file diff --git a/lib/models/metadata.js b/lib/models/metadata.js new file mode 100644 index 0000000..f86fa9a --- /dev/null +++ b/lib/models/metadata.js @@ -0,0 +1,55 @@ +import { DataTypes } from "sequelize" + +export default function (sequelize) { + const Metadata = sequelize.define('Metadata', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + title: { + type: DataTypes.STRING, + allowNull: false + }, + alternatetitles: { + type: DataTypes.STRING + }, + description: { + type: DataTypes.STRING + }, + rating: { + type: DataTypes.STRING + }, + coverarturl: { + type: DataTypes.STRING + }, + releasedate: { + type: DataTypes.DATE + }, + igdbid: { + type: DataTypes.INTEGER + }, + timetobeat: { + type: DataTypes.STRING + }, + genre: { + type: DataTypes.STRING + }, + developers: { + type: DataTypes.STRING + }, + publishers: { + type: DataTypes.STRING + }, + gamemodes:{ + type: DataTypes.STRING + } + }, { + indexes: [ + { fields: ['title'] }, + { fields: ['description'] }//If this slows down the db may want to not index this. + ] + }) + + return Metadata +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 70bdd6a..824ba88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,8 @@ "ejs": "^3.1.10", "express": "^4.21.1", "figlet": "^1.7.0", - "file-older-than": "^1.0.0", "i18n": "^0.15.1", + "igdb-api-node": "^6.0.5", "innertext": "^1.0.3", "jsdom": "^25.0.1", "jszip": "^3.10.1", @@ -614,6 +614,17 @@ "arrow2csv": "bin/arrow2csv.js" } }, + "node_modules/apicalypse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/apicalypse/-/apicalypse-1.0.5.tgz", + "integrity": "sha512-RT6PRNlYrQTrDBdifhlUtww1codmjUH5ZFBxyWS0itkkInzO0z/Xxa8RBV7/TcEOiwIzfkFSt9Hd0Nu3ScVsFQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.3", + "better-queue": "^3.8.10", + "better-queue-memory": "^1.0.4" + } + }, "node_modules/array-back": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", @@ -650,12 +661,53 @@ "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", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/better-queue": { + "version": "3.8.12", + "resolved": "https://registry.npmjs.org/better-queue/-/better-queue-3.8.12.tgz", + "integrity": "sha512-D9KZ+Us+2AyaCz693/9AyjTg0s8hEmkiM/MB3i09cs4MdK1KgTSGJluXRYmOulR69oLZVo2XDFtqsExDt8oiLA==", + "license": "MIT", + "dependencies": { + "better-queue-memory": "^1.0.1", + "node-eta": "^0.9.0", + "uuid": "^9.0.0" + } + }, + "node_modules/better-queue-memory": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/better-queue-memory/-/better-queue-memory-1.0.4.tgz", + "integrity": "sha512-SWg5wFIShYffEmJpI6LgbL8/3Dqhku7xI1oEiy6FroP9DbcZlG0ZDjxvPdP9t7hTGW40IpIcC6zVoGT1oxjOuA==", + "license": "MIT" + }, + "node_modules/better-queue/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -1280,12 +1332,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 +1649,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 +1715,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", @@ -2088,6 +2145,15 @@ "node": ">=0.10.0" } }, + "node_modules/igdb-api-node": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/igdb-api-node/-/igdb-api-node-6.0.5.tgz", + "integrity": "sha512-rH45b1vJNOusoTt8KkYlqrz4bqBXfH06uezOL+uIPuExyRnbRGNYUSUsGlcIx8YFs0E5U2FmDrlsRqbe9zHhVA==", + "license": "MIT", + "dependencies": { + "apicalypse": "^1.0.4" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -2564,6 +2630,12 @@ "node": ">=10.5.0" } }, + "node_modules/node-eta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/node-eta/-/node-eta-0.9.0.tgz", + "integrity": "sha512-mTCTZk29tmX1OGfVkPt63H3c3VqXrI2Kvua98S7iUIB/Gbp0MNw05YtUomxQIxnnKMyRIIuY9izPcFixzhSBrA==", + "license": "MIT" + }, "node_modules/node-fetch": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", @@ -2902,6 +2974,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", diff --git a/package.json b/package.json index 8ed2d21..39b49b7 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "ejs": "^3.1.10", "express": "^4.21.1", "figlet": "^1.7.0", - "file-older-than": "^1.0.0", "i18n": "^0.15.1", + "igdb-api-node": "^6.0.5", "innertext": "^1.0.3", "jsdom": "^25.0.1", "jszip": "^3.10.1", diff --git a/views/partials/result.ejs b/views/partials/result.ejs index 9be8684..68c6d78 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -1,6 +1,6 @@
    - "> + ">

    <%= result.title || result.filename %>

    diff --git a/views/public/images/nocoverart.png b/views/public/images/coverart/nocoverart.png similarity index 100% rename from views/public/images/nocoverart.png rename to views/public/images/coverart/nocoverart.png diff --git a/views/public/images/testcover.webp b/views/public/images/coverart/testcover.webp similarity index 100% rename from views/public/images/testcover.webp rename to views/public/images/coverart/testcover.webp From b5539131792f520d8360b824933b2e514e32eb08 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 22 May 2025 11:09:01 -0600 Subject: [PATCH 03/46] env change --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index ac3e62c..5519058 100644 --- a/.env +++ b/.env @@ -27,4 +27,4 @@ ELASTICSEARCH_URL=http://localhost:9200 #IGDB Connection Configuration TWITCH_CLIENT_ID= -TWITCH_APP_ACCESS_TOKEN= \ No newline at end of file +TWITCH_CLIENT_SECRET= \ No newline at end of file From 99a370fdeaf17b0f5252621475b0f830e46242ad Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 22 May 2025 16:52:12 -0600 Subject: [PATCH 04/46] incremental commit --- lib/categories.json | 2 +- lib/metadataworker.js | 114 +++++++++++++++++++++++++++++++++++++++++- package-lock.json | 77 ++++++++-------------------- package.json | 2 +- 4 files changed, 134 insertions(+), 61 deletions(-) diff --git a/lib/categories.json b/lib/categories.json index 765479c..45125a2 100644 --- a/lib/categories.json +++ b/lib/categories.json @@ -215,7 +215,7 @@ "Game Boy Advance", "Game Boy Color", "Game Boy", - "Gamecube", + "GameCube", "Kiosk Video Compact Flash", "Misc", "New Nintendo 3DS", diff --git a/lib/metadataworker.js b/lib/metadataworker.js index 77e70c5..1cdee90 100644 --- a/lib/metadataworker.js +++ b/lib/metadataworker.js @@ -1,3 +1,113 @@ -import igdb from 'igdb-api-node'; +import { twitchAccessToken, igdb, request } from "@phalcode/ts-igdb-client"; +import { + fields, + or, + and, + where, + whereIn, + WhereFlags, + WhereInFlags, + sort, + limit, + offset, +} from "@phalcode/ts-igdb-client"; -function getMetadata() \ No newline at end of file +const twitchSecrets = { + client_id: process.env.TWITCH_CLIENT_ID, + client_secret: process.env.TWITCH_CLIENT_SECRET, +}; +const accessToken = await twitchAccessToken(twitchSecrets); + +const client = igdb(twitchSecrets.client_id, accessToken); + +const 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", + "multiplayer_modes.*", + "game_localizations.name", + "game_localizations.region", + "platforms.name", +]; + +export async function getMetadata(query) { + const data = await client + .request("games") + .pipe( + fields(gameFields), + or(...buildOrAndClauses("name", "~", query)), + sort("name", "asc") + ) + .execute(); + return data; +} + +function buildOrClauses(field, op, queries) { + let orClauses = []; + for (let x in queries) { + orClauses.push(where(field, op, queries[x], WhereFlags.CONTAINS)); + } + return orClauses; +} + +function buildOrAndClauses(field, op, queries) { + let orClauses = []; + + for (let x in queries) { + let name = queries[x].split(" "); + let andClauses = []; + for (let y in name) { + andClauses.push(where(field, op, name[y], WhereFlags.CONTAINS)); + } + orClauses.push(and(...andClauses)); + } + return orClauses; +} + +function normalizeName(filename) { + if (!filename) return; + return filename + .replace(/\.[A-z]{3,3}|\.|&|-|,|v[0-9]+\.[0-9]+|\[.*?\]|\(.*?\)/g, "") + .trim(); +} + +function getBestMatch(filename, data) { + const words = filename.split(" "); + let bestIndex = null; + let bestMatchCount = 0; + let lengthDifference = 0; + for (let x in data) { + let dataWords = data[x].name.split(" "); + let matchingWords = 0; + for (let y in words) { + if (y >= dataWords.length) break; + if (words[y] == dataWords[y]) matchingWords++; + } + let diff = matchingWords - dataWords.length; + if (matchingWords > bestMatchCount && diff < lengthDifference) { + bestIndex = x; + bestMatchCount = matchingWords; + lengthDifference = diff + } + } + if(bestIndex != null){ + return data[bestIndex] + } + return +} + +let games = await getMetadata([ + "The Legend of Zelda A Link to the Past", + "Super Mario Sunshine", +]); +console.log(JSON.stringify(games.data, null, 2)); +//console.log(await getMetadata(games)) diff --git a/package-lock.json b/package-lock.json index 824ba88..4eed92c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "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", @@ -13,7 +14,6 @@ "express": "^4.21.1", "figlet": "^1.7.0", "i18n": "^0.15.1", - "igdb-api-node": "^6.0.5", "innertext": "^1.0.3", "jsdom": "^25.0.1", "jszip": "^3.10.1", @@ -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", @@ -614,17 +633,6 @@ "arrow2csv": "bin/arrow2csv.js" } }, - "node_modules/apicalypse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/apicalypse/-/apicalypse-1.0.5.tgz", - "integrity": "sha512-RT6PRNlYrQTrDBdifhlUtww1codmjUH5ZFBxyWS0itkkInzO0z/Xxa8RBV7/TcEOiwIzfkFSt9Hd0Nu3ScVsFQ==", - "license": "MIT", - "dependencies": { - "axios": "^1.7.3", - "better-queue": "^3.8.10", - "better-queue-memory": "^1.0.4" - } - }, "node_modules/array-back": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", @@ -678,36 +686,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/better-queue": { - "version": "3.8.12", - "resolved": "https://registry.npmjs.org/better-queue/-/better-queue-3.8.12.tgz", - "integrity": "sha512-D9KZ+Us+2AyaCz693/9AyjTg0s8hEmkiM/MB3i09cs4MdK1KgTSGJluXRYmOulR69oLZVo2XDFtqsExDt8oiLA==", - "license": "MIT", - "dependencies": { - "better-queue-memory": "^1.0.1", - "node-eta": "^0.9.0", - "uuid": "^9.0.0" - } - }, - "node_modules/better-queue-memory": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/better-queue-memory/-/better-queue-memory-1.0.4.tgz", - "integrity": "sha512-SWg5wFIShYffEmJpI6LgbL8/3Dqhku7xI1oEiy6FroP9DbcZlG0ZDjxvPdP9t7hTGW40IpIcC6zVoGT1oxjOuA==", - "license": "MIT" - }, - "node_modules/better-queue/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -2145,15 +2123,6 @@ "node": ">=0.10.0" } }, - "node_modules/igdb-api-node": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/igdb-api-node/-/igdb-api-node-6.0.5.tgz", - "integrity": "sha512-rH45b1vJNOusoTt8KkYlqrz4bqBXfH06uezOL+uIPuExyRnbRGNYUSUsGlcIx8YFs0E5U2FmDrlsRqbe9zHhVA==", - "license": "MIT", - "dependencies": { - "apicalypse": "^1.0.4" - } - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -2630,12 +2599,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-eta": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-eta/-/node-eta-0.9.0.tgz", - "integrity": "sha512-mTCTZk29tmX1OGfVkPt63H3c3VqXrI2Kvua98S7iUIB/Gbp0MNw05YtUomxQIxnnKMyRIIuY9izPcFixzhSBrA==", - "license": "MIT" - }, "node_modules/node-fetch": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", diff --git a/package.json b/package.json index 39b49b7..29d1dba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "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", @@ -8,7 +9,6 @@ "express": "^4.21.1", "figlet": "^1.7.0", "i18n": "^0.15.1", - "igdb-api-node": "^6.0.5", "innertext": "^1.0.3", "jsdom": "^25.0.1", "jszip": "^3.10.1", From 2f2f358ea2366bf47fc56133f7c068bdf6ee631a Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 22 May 2025 17:00:48 -0600 Subject: [PATCH 05/46] change best match to be less word order dependent. dedupe repeat terms --- lib/metadataworker.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/metadataworker.js b/lib/metadataworker.js index 1cdee90..ddd3102 100644 --- a/lib/metadataworker.js +++ b/lib/metadataworker.js @@ -63,7 +63,7 @@ function buildOrAndClauses(field, op, queries) { let orClauses = []; for (let x in queries) { - let name = queries[x].split(" "); + let name = [...new Set(queries[x].split(" "))]; //dedupe; let andClauses = []; for (let y in name) { andClauses.push(where(field, op, name[y], WhereFlags.CONTAINS)); @@ -86,23 +86,23 @@ function getBestMatch(filename, data) { let bestMatchCount = 0; let lengthDifference = 0; for (let x in data) { - let dataWords = data[x].name.split(" "); let matchingWords = 0; for (let y in words) { - if (y >= dataWords.length) break; - if (words[y] == dataWords[y]) matchingWords++; + if (data[x].name.toLowerCase().includes(words[y].toLowerCase())) + matchingWords++; } let diff = matchingWords - dataWords.length; if (matchingWords > bestMatchCount && diff < lengthDifference) { bestIndex = x; bestMatchCount = matchingWords; - lengthDifference = diff + lengthDifference = diff; + if (lengthDifference < 0) lengthDifference = 0; } } - if(bestIndex != null){ - return data[bestIndex] + if (bestIndex != null) { + return data[bestIndex]; } - return + return; } let games = await getMetadata([ From a863c46d5699fb389b9980b5b08515abbd7b3c8c Mon Sep 17 00:00:00 2001 From: Brady <159680318+V0xelle@users.noreply.github.com> Date: Fri, 23 May 2025 02:47:26 -0500 Subject: [PATCH 06/46] Create flag_us.png --- views/public/images/flags/flag_us.png | 1 + 1 file changed, 1 insertion(+) create mode 100644 views/public/images/flags/flag_us.png diff --git a/views/public/images/flags/flag_us.png b/views/public/images/flags/flag_us.png new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/views/public/images/flags/flag_us.png @@ -0,0 +1 @@ + From 023f5f5a1efd54a0a3a5edc5cc369d73cfaa759e Mon Sep 17 00:00:00 2001 From: Brady <159680318+V0xelle@users.noreply.github.com> Date: Fri, 23 May 2025 02:48:02 -0500 Subject: [PATCH 07/46] Add files via upload --- views/public/images/flags/flag_us.png | Bin 1 -> 224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/views/public/images/flags/flag_us.png b/views/public/images/flags/flag_us.png index 8b137891791fe96927ad78e64b0aad7bded08bdc..b64d8d99d5f4ba673cea121b6dfd1b2f820d5f88 100644 GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^MnEjb!3HGr0)77gDaPU;cPEB*=VV?2IkP-n977^n z-%dHm*O`B<}> zo6obaeJOXnQ|XZ@ASn3$d;C|4uV0*65M)mlb>g9 z_FBa$FTM-rt+l?kIMiRs`~GT`BwyRkCv#?U2$it#S~aoBIyUz$aCE=0Ksn=rpb6i% X{(wslOz&?6x{|@u)z4*}Q$iB}@Wob> literal 1 Icmd-A000XB3jhEB From 10e1d6a6b9a32574342ed4169794772ccba2e0f9 Mon Sep 17 00:00:00 2001 From: Brady <159680318+V0xelle@users.noreply.github.com> Date: Fri, 23 May 2025 02:50:25 -0500 Subject: [PATCH 08/46] Rename flag_us.png to USA.png --- views/public/images/flags/{flag_us.png => USA.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename views/public/images/flags/{flag_us.png => USA.png} (100%) diff --git a/views/public/images/flags/flag_us.png b/views/public/images/flags/USA.png similarity index 100% rename from views/public/images/flags/flag_us.png rename to views/public/images/flags/USA.png From d4a2bbfb63d87bb282a1cbda5750db6e3fc54747 Mon Sep 17 00:00:00 2001 From: Brady <159680318+V0xelle@users.noreply.github.com> Date: Fri, 23 May 2025 02:56:07 -0500 Subject: [PATCH 09/46] Add files via upload --- views/public/images/flags/Europe.png | Bin 0 -> 281 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 views/public/images/flags/Europe.png diff --git a/views/public/images/flags/Europe.png b/views/public/images/flags/Europe.png new file mode 100644 index 0000000000000000000000000000000000000000..ffee3b7285de073c7770e406bc98fbc6ba8dc652 GIT binary patch literal 281 zcmV+!0p|XRP)Px#(@8`@R9J=Wn86LgFbqXqK%BTRK_}=wor*cSPY3A;aEeq_X#n?q2-NS?i+}uO zr%uvb+S}d!fWx;Fpa>(cGL3^x Date: Fri, 23 May 2025 02:59:38 -0500 Subject: [PATCH 10/46] Fixed Europe --- views/public/images/flags/Europe.png | Bin 281 -> 257 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/views/public/images/flags/Europe.png b/views/public/images/flags/Europe.png index ffee3b7285de073c7770e406bc98fbc6ba8dc652..34a0ee396d6d99a4096c9bfa0165c7e642154326 100644 GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^#y~93!3HF~&VNk@QjEnx?oJHr&dIz4a&~*VIEF;D zzMXMU)WJZ&ML1ntbcKWcs-X5dkwq+edyXgW-9CG!>%DVFa~~NeZQA*Exo+s9XK$q) zPO+xNruC=(Fi`Jc7m!^heeCvBO^u!}UsMIY+-^FU@%TuGJdc~Sy4#hTEJ%o_r_s@iZCcoa^@tMT#;Wf2%vuxA80;T(2N8bUx!QkoY=d#Wzp$Pzi Cx@dI( literal 281 zcmV+!0p|XRP)Px#(@8`@R9J=Wn86LgFbqXqK%BTRK_}=wor*cSPY3A;aEeq_X#n?q2-NS?i+}uO zr%uvb+S}d!fWx;Fpa>(cGL3^x Date: Fri, 23 May 2025 03:06:37 -0500 Subject: [PATCH 11/46] Fixed Europe --- views/public/images/flags/Europe.png | Bin 257 -> 266 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/views/public/images/flags/Europe.png b/views/public/images/flags/Europe.png index 34a0ee396d6d99a4096c9bfa0165c7e642154326..2b9d636291a38b100ada7b3653d9cf318c6a7627 100644 GIT binary patch delta 225 zcmV<703QE=0*V5VF@MBKL_t(oh3%NZ4S+BRMO)8aoWKd($ElpdeH_FQ^kkxqm=-=* z4e#QmL0(D=8ry!_!cS=4P5_C^l(H_f#cT9s4iO6>xJ;UKH;k7jwGIJPhS~*61T6%W zp>~10u9WC~<^-x#5OG8@sM<%x5y_xx9~H;XOs%kGv^u&ZE?P@QtLaJOvShTHo;0ps z0JY*1K$?J0019#Z0(c^~R6hHq$z4~9$(LsCe*r1#XXY$|PCiAwX=?4l9VQcig-8fK b&eY2ejzMnOK}(8H00000NkvXXu0mjf%P?b< delta 216 zcmV;}04M*70)YaMF@L*BL_t(oh3%NZ6+kc$L=$Hh6{tWzs#1r36ruz!yvJd^x0{(o z-p<8s;xl5hx$UPdRiftY8$ikhW?g2FW%w{BhzAfIrW?lFl_@BU^zKT%cP-%*{0V?c7X~2S`elb+a9UzHlPn SO2zH~0000 Date: Fri, 23 May 2025 03:28:36 -0500 Subject: [PATCH 12/46] Add Germany, Italy, Korea --- views/public/images/flags/Germany.png | Bin 0 -> 156 bytes views/public/images/flags/Italy.png | Bin 0 -> 156 bytes views/public/images/flags/Korea.png | Bin 0 -> 486 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 views/public/images/flags/Germany.png create mode 100644 views/public/images/flags/Italy.png create mode 100644 views/public/images/flags/Korea.png diff --git a/views/public/images/flags/Germany.png b/views/public/images/flags/Germany.png new file mode 100644 index 0000000000000000000000000000000000000000..1426fa3d8fb0d275d6be039448c2cd2ae5ba9832 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^#y~93!3HF~&VNk@QjEnx?oJHr&dIz4a>6`a977^n z-(Ee)dB8w`+0k;xvM=$I?=vpvY!=@bd{DRQj>o3pM>}f|cpeLobWY`&Y8bNl+27?k zlF2K2786`a977^n z-<~(*Wl#_~65zniB+w?E^N_*Nz_`Ax?aSNgF~zc_%NwqSr_{|{+xqS4MShm#6+H`$ j+D=JmVQ2i(ddBc_DQjB%#V6)K8yGxY{an^LB{Ts59gsGN literal 0 HcmV?d00001 diff --git a/views/public/images/flags/Korea.png b/views/public/images/flags/Korea.png new file mode 100644 index 0000000000000000000000000000000000000000..e6c62a5230b2b35baf495629ef2756ce34202117 GIT binary patch literal 486 zcmV@P)Px$ph-kQRA_xINu601{rIYp6x{yj&Zzpe2-u%seX5rKnL}Ucec_a-3Y*o7)K50HsF}VZM`2 zM1}Qbzkhi4e!ZIciRjPUQiEq{!LcT_c$pJhq9wk?sf{vmc-ImKN^ox;GO3x)w1z|~ zSUca7y~W{efmZMul5=jwEI5#OJAAv=d!nQV@s$;U)9c5>v(MXCIB#fpbE0}lp3dm4m!`saLft)EvG7?37Ht>UqIyS5J1l`*%Aq~u!R()k_(h(&w) zRZ=0XdC0z;skAJAYf3xrpPIxd-qQB&U7CUeN$i2AmQKl$$RgS}9JtW+l$9wdMWY&X zR-g=^_H?gZ5}Do{kFZ63CokZWpaLQjQ^;{ShA(AO**WjE&?7?UDUg%z|Sp cn4-6WU#3GGPC${a_W%F@07*qoM6N<$f*jS~od5s; literal 0 HcmV?d00001 From 14ec01c8ff732b3089bce34cc5d963b925ddf500 Mon Sep 17 00:00:00 2001 From: Brady <159680318+V0xelle@users.noreply.github.com> Date: Fri, 23 May 2025 04:08:48 -0500 Subject: [PATCH 13/46] Updated flags --- views/public/images/flags/Spain.png | Bin 0 -> 450 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 views/public/images/flags/Spain.png diff --git a/views/public/images/flags/Spain.png b/views/public/images/flags/Spain.png new file mode 100644 index 0000000000000000000000000000000000000000..bde63c7f46a8be7ef060a9deb9a56408fe106b7d GIT binary patch literal 450 zcmV;z0X_bSP)Px$d`Uz>R9J=Wm_19wP!xurYOB-_L>C1eGBg&)PC~Iiz^QZ>zosls{siaF#!aPr zM}I&;a51hWLOUo_K|53_T8s{9Q>umLc_ndpUvD;MYn~*RuZU;mX zi3uu^n4l70JPwZ@PfALSHl;>elrNr%yZJ~`ssJ2St^iobZ39qO(}LV`hhs64Ql+k@ zndTt%i?G?>e2eCQ1TrsWmhDh84w)Hlb(YNzWX+nHw30H9Ya zB&CWb%RH`q0kHZoNk-SLsQk9~3tM8YljfLh|6S`bY%l9`ZLoFBo?g4>C#hP0eWSaV z(e(k_x|$xa_ml8kGDKqf9B%EBIXDsJi)S2{=Fa|ne{ydQnk-vO<7|&y-7`JTQe_h7 zEkUk$Csa9OD>BPV-4bi_-f(I!ijiUpH#-CK@@9$WmstR2@|Tc@PN4vp`7+GjFKl%H s|AkQHOsK@fqDR1h5{U^ak(i+J1OD!3LmEotpa1{>07*qoM6N<$f`~T9T>t<8 literal 0 HcmV?d00001 From 15fca13753b50f0aaf764036c67b62ad595f45ae Mon Sep 17 00:00:00 2001 From: Brady <159680318+V0xelle@users.noreply.github.com> Date: Fri, 23 May 2025 04:12:38 -0500 Subject: [PATCH 14/46] Added France, Japan --- views/public/images/flags/France.png | Bin 0 -> 155 bytes views/public/images/flags/Japan.png | Bin 0 -> 216 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 views/public/images/flags/France.png create mode 100644 views/public/images/flags/Japan.png diff --git a/views/public/images/flags/France.png b/views/public/images/flags/France.png new file mode 100644 index 0000000000000000000000000000000000000000..b0a7d38c4151fbe884f0ebfa02e3c02b1c0d9df9 GIT binary patch literal 155 zcmeAS@N?(olHy`uVBq!ia0vp^#y~93!3HF~&VNk@QjEnx?oJHr&dIz4azZ^_977^n z-=06n%OD`a68J?~>(ZP_R~GP}J9j@(>GQkUZ=OUQ_hT&ncH+;bDDJnX7t3=T3y^e9 j<(X<2f}Qa-^b8}1Bx}vd-BL|J3m80I{an^LB{Ts5uQD}K literal 0 HcmV?d00001 diff --git a/views/public/images/flags/Japan.png b/views/public/images/flags/Japan.png new file mode 100644 index 0000000000000000000000000000000000000000..e39bfb407e243751c83911236e5ae9504b1666fe GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^MnEjb!3HGr0)77gDaPU;cPEB*=VV?2Ig>nH977^n z-%fYrYB1noS^HzL-O=)2Q}$`j#iVb9Gi4c6R76Mx^g&;OeSqR=RgX zr(53g{<`l$XXZ`S>5JE0uu-6?YYU5shvJQ%nyD}LKJPR*%qJ-MQb$WvEy%G%B(S*? zs7>^UJ1>wGBl%Jz%QsPJ*1U5{)7{p6%>VJX|0<8gn|lj7%nx Date: Sat, 24 May 2025 00:30:00 -0500 Subject: [PATCH 15/46] Update region flags --- views/public/images/flags/Asia.png | Bin 0 -> 448 bytes views/public/images/flags/Brazil.png | Bin 0 -> 403 bytes views/public/images/flags/UK.png | Bin 0 -> 440 bytes views/public/images/flags/World.png | Bin 0 -> 385 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 views/public/images/flags/Asia.png create mode 100644 views/public/images/flags/Brazil.png create mode 100644 views/public/images/flags/UK.png create mode 100644 views/public/images/flags/World.png diff --git a/views/public/images/flags/Asia.png b/views/public/images/flags/Asia.png new file mode 100644 index 0000000000000000000000000000000000000000..a3f32b56e096971c501105565368f86d8587b570 GIT binary patch literal 448 zcmV;x0YCnUP)Px$dPzhUR`lsUBei)OXG7k z>-5*hqx!dD+xZFbb}bRon1m+sXNa&?Oc26%p0`WaiiW5rro%F>4H|m}h}-=v0tiji z$QY&uZqN+ynR%!v$zhX0yG7&sazndrXMP$KO(X z4P@_;pC;#UoEu&ot?IF qhX4s77CTMZbuFmuH;39H0N@wHKfSUrxYg1C0000 literal 0 HcmV?d00001 diff --git a/views/public/images/flags/Brazil.png b/views/public/images/flags/Brazil.png new file mode 100644 index 0000000000000000000000000000000000000000..abc33aeca2232b62e3621767345d6c6f852fe194 GIT binary patch literal 403 zcmV;E0c`$>P)Px$O-V#SR9J=Wm^}`{Fbsta;t-vfIew6g7_1CAKzCLom?Er_A1Ag$ zfxZb#keEA8DVKt!x%6Yi0Ht85 zHHDzOFj5Uo3vBEfFOV0;43l_=mITXk20x%NcKE>Vw?6q?>YE{#<`k4xX$GK|XL4!Q z9Nnej{_g}Z7iTsDkRLW#gQhPqrTK;K?1f!~MF`87B-?qfb>i1gzC?*W=lx>IonOq>4WEdQkuX002ovPDHLkV1kdftKI+r literal 0 HcmV?d00001 diff --git a/views/public/images/flags/UK.png b/views/public/images/flags/UK.png new file mode 100644 index 0000000000000000000000000000000000000000..713567d6c060b4b05e5923a89794ac304a25a637 GIT binary patch literal 440 zcmV;p0Z0CcP)Px$a!Eu%R9J=Wn89(wAP_~rGSuM`bP^pz4MdYxQbcClh&8FeC2+DDB8LQL+UW1CbE)f^DvvR1C&=A1S z4Kn>OUPI7OH3y=Vr&gKp0l+L&ja8!6;HHM5jcKTw)Z>)m#`D5m+z%sK=P;X3zkj$j zx7E0}co_Cu>|Z5mH7L{VyY i#9k?z+LV&QsrL=#AY+7|2?g%}0000Px$J4r-AR9J=Wn9UJ_Fc5`T$Gaui0yWrx-tB;c#dxp-?$7`=*n%Zc0iH60AshA= zoQ&Twz$TA<*}wu6#Lgq=?fxFIYt7^2GADqcn4AC%YhfnABtim~f>ePp5y$IgHa4_f zay;-nztp0{0XW&niI}1lBzia}gt~AHji9}1<_N72E$p|z*(Xw@*Pdq3vd4 zG2tHUx Date: Sat, 24 May 2025 02:40:43 -0600 Subject: [PATCH 16/46] it works but it's a mess --- lib/database.js | 2 +- lib/dboptimize.js | 4 +- lib/dircrawl.js | 7 +- lib/fileworker.js | 29 ++- lib/metadatasearch.js | 223 ++++++++++++++++ lib/metadataworker.js | 113 --------- lib/models/file.js | 5 + lib/models/metadata.js | 18 +- lib/nonGameTerms.json | 2 + lib/services/elasticsearch.js | 240 ++++++++---------- lib/time.js | 7 +- package-lock.json | 8 +- package.json | 2 +- server.js | 26 +- views/pages/results.ejs | 97 +------ .../pages/{resultsnew.ejs => resultsold.ejs} | 88 ++++++- views/partials/opengraphresults.ejs | 7 +- views/partials/result.ejs | 19 +- 18 files changed, 519 insertions(+), 378 deletions(-) create mode 100644 lib/metadatasearch.js delete mode 100644 lib/metadataworker.js rename views/pages/{resultsnew.ejs => resultsold.ejs} (66%) diff --git a/lib/database.js b/lib/database.js index 768e588..1c8552d 100644 --- a/lib/database.js +++ b/lib/database.js @@ -18,7 +18,7 @@ export const File = defineFile(sequelize); export const QueryCount = defineQueryCount(sequelize); export const Metadata = defineMetadata(sequelize) Metadata.hasMany(File) -File.belongsTo(Metadata) +File.belongsTo(Metadata, {as: "details"}) export async function initDB() { try { diff --git a/lib/dboptimize.js b/lib/dboptimize.js index 80c08e4..4b9f764 100644 --- a/lib/dboptimize.js +++ b/lib/dboptimize.js @@ -5,7 +5,7 @@ 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 "./time.js"; let piscina = new Piscina({ filename: resolve("./lib", "dbkwworker.js"), @@ -28,7 +28,7 @@ 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(); diff --git a/lib/dircrawl.js b/lib/dircrawl.js index b8d379a..4ac9809 100644 --- a/lib/dircrawl.js +++ b/lib/dircrawl.js @@ -5,7 +5,7 @@ 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 { Timer } from "./time.js"; let piscina = new Piscina({ filename: resolve("./lib", "fileworker.js"), @@ -15,7 +15,7 @@ let piscina = new Piscina({ 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 = []; @@ -161,7 +161,8 @@ async function processBatch(files) { type: file.type, date: file.date, region: file.region, - group: file.group + group: file.group, + nongame: file.nongame })), { returning: true, diff --git a/lib/fileworker.js b/lib/fileworker.js index d5dde1c..ebfceb1 100644 --- a/lib/fileworker.js +++ b/lib/fileworker.js @@ -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, '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}`); diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js new file mode 100644 index 0000000..c339f16 --- /dev/null +++ b/lib/metadatasearch.js @@ -0,0 +1,223 @@ +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.js"; +import { Sequelize } from "sequelize"; + +export default class MetadataSearch { + constructor() { + this.twitchSecrets = { + client_id: process.env.TWITCH_CLIENT_ID, + client_secret: process.env.TWITCH_CLIENT_SECRET, + }; + this.setupClient(); + } + 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", + "platforms.name", + "game_type.type", + ]; + + 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); + if (this.accessToken) { + this.authorized = true; + return; + } + } + this.authorized = false; //disable + } catch (error) { + this.authorized = false; + } + } + + async getMetadata(query, retrying = false) { + try { + if (!this.authorized) return; + const { data } = await this.client + .multi(...this.buildGameMultiQuery(query)) + .execute(); + return data; + } catch (error) { + if (error === "ERR_BAD_REQUEST" && !retrying) { + this.setupClient(); + return this.getMetadata(query, true); + } + console.error("Failed to retrieve metadata:", error); + } + } + + buildGameMultiQuery(query) { + let multiQuery = []; + for (let x in query) { + multiQuery.push( + request("games") + .alias(x) + .pipe( + fields(this.gameFields), + and( + ...this.buildAndClauses("name", "~", query[x].name), + where("game_type.type", "=", "Main Game"), + where("platforms.name", "~", query[x].platform) + ), + limit(1) + ) + ); + } + return multiQuery; + } + + buildAndClauses(field, op, string) { + let andClauses = []; + let name = [...new Set(string.split(" "))].filter((n) => n); //dedupe; + for (let x in name) { + andClauses.push(where(field, op, name[x], WhereFlags.CONTAINS)); + } + return andClauses; + } + + normalizeName(filename) { + if (!filename) return; + return filename + .replace(/\.[A-z]{3,3}|\.|&|-|,|v[0-9]+\.[0-9]+|\[.*?\]|\(.*?\)/g, "") + .trim(); + } + + async getGamesMetadata(games) { + try { + if (!this.authorized || !games.length) return []; + let gameQuery = []; + for (let x in games) { + if (!(await games[x].getDetails())) + if (!games[x].nongame) { + if (!games[x].blockmetadata) { + gameQuery.push({ + name: this.normalizeName(games[x].filename), + platform: games[x].category, + id: x, + }); + } + } + } + if (!gameQuery.length) return []; + let gameMetas = await this.getMetadata(gameQuery); + if (!gameMetas.length) return []; + for (let x in gameMetas) { + if (gameMetas[x].result.length) { + await this.addMetadataToDb( + gameMetas[x].result[0], + games[gameQuery[x].id] + ); + } + } + let details = await Promise.all(games.map((game) => game.getDetails())); + return details.map((details) => details?.dataValues); + } catch (error) { + console.error("Error getting metadata:", error); + } + } + + async addMetadataToDb(metadata, game) { + try { + let md = await Metadata.findOne({ + where: { + id: metadata.id, + }, + }); + if (!md) { + md = await Metadata.build( + { + id: metadata.id, + title: metadata.name, + + description: metadata.summary, + rating: metadata.total_rating, + coverartid: metadata.cover?.image_id, + releasedate: metadata.first_release_date + ? new Date(metadata.first_release_date * 1000) + : null, + genre: JSON.stringify(metadata.genres?.map((genre) => genre.name)), + gamemodes: JSON.stringify( + metadata.game_modes?.map((gm) => gm.name) + ), + platforms: JSON.stringify( + metadata.platforms?.map((platform) => platform.name) + ), + }, + { + returning: true, + updateOnDuplicate: ["id"], + include: File, + } + ); + } + //these don't work right unless I do them after the fact. + md.developers = JSON.stringify( + metadata.involved_companies + ?.filter((ic) => ic.developer) + ?.map((ic) => ic.company.name) + ); + md.publishers = JSON.stringify( + metadata.involved_companies + ?.filter((ic) => ic.publisher) + ?.map((ic) => ic.company.name) + ); + let alternates = []; + if (metadata.alternative_names) { + alternates.push( + metadata.alternative_names.map((an) => ({ + type: an.comment, + name: an.name, + })) + ); + } + if (metadata.game_localizations) { + alternates.push( + metadata.game_localizations.map((gn) => ({ + type: gn.region.name, + name: gn.name, + })) + ); + } + md.alternatetiles = JSON.stringify(alternates); + await md.save(); + await game.setDetails(md); + await md.addFile(game); + } catch (error) { + console.error("Error adding metadata:", error); + } + } +} diff --git a/lib/metadataworker.js b/lib/metadataworker.js deleted file mode 100644 index ddd3102..0000000 --- a/lib/metadataworker.js +++ /dev/null @@ -1,113 +0,0 @@ -import { twitchAccessToken, igdb, request } from "@phalcode/ts-igdb-client"; -import { - fields, - or, - and, - where, - whereIn, - WhereFlags, - WhereInFlags, - sort, - limit, - offset, -} from "@phalcode/ts-igdb-client"; - -const twitchSecrets = { - client_id: process.env.TWITCH_CLIENT_ID, - client_secret: process.env.TWITCH_CLIENT_SECRET, -}; -const accessToken = await twitchAccessToken(twitchSecrets); - -const client = igdb(twitchSecrets.client_id, accessToken); - -const 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", - "multiplayer_modes.*", - "game_localizations.name", - "game_localizations.region", - "platforms.name", -]; - -export async function getMetadata(query) { - const data = await client - .request("games") - .pipe( - fields(gameFields), - or(...buildOrAndClauses("name", "~", query)), - sort("name", "asc") - ) - .execute(); - return data; -} - -function buildOrClauses(field, op, queries) { - let orClauses = []; - for (let x in queries) { - orClauses.push(where(field, op, queries[x], WhereFlags.CONTAINS)); - } - return orClauses; -} - -function buildOrAndClauses(field, op, queries) { - let orClauses = []; - - for (let x in queries) { - let name = [...new Set(queries[x].split(" "))]; //dedupe; - let andClauses = []; - for (let y in name) { - andClauses.push(where(field, op, name[y], WhereFlags.CONTAINS)); - } - orClauses.push(and(...andClauses)); - } - return orClauses; -} - -function normalizeName(filename) { - if (!filename) return; - return filename - .replace(/\.[A-z]{3,3}|\.|&|-|,|v[0-9]+\.[0-9]+|\[.*?\]|\(.*?\)/g, "") - .trim(); -} - -function getBestMatch(filename, data) { - const words = filename.split(" "); - let bestIndex = null; - let bestMatchCount = 0; - let lengthDifference = 0; - for (let x in data) { - let matchingWords = 0; - for (let y in words) { - if (data[x].name.toLowerCase().includes(words[y].toLowerCase())) - matchingWords++; - } - let diff = matchingWords - dataWords.length; - if (matchingWords > bestMatchCount && diff < lengthDifference) { - bestIndex = x; - bestMatchCount = matchingWords; - lengthDifference = diff; - if (lengthDifference < 0) lengthDifference = 0; - } - } - if (bestIndex != null) { - return data[bestIndex]; - } - return; -} - -let games = await getMetadata([ - "The Legend of Zelda A Link to the Past", - "Super Mario Sunshine", -]); -console.log(JSON.stringify(games.data, null, 2)); -//console.log(await getMetadata(games)) diff --git a/lib/models/file.js b/lib/models/file.js index b3f0467..754ba21 100644 --- a/lib/models/file.js +++ b/lib/models/file.js @@ -55,6 +55,11 @@ export default function (sequelize) { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false + }, + nongame: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false } }, { indexes: [ diff --git a/lib/models/metadata.js b/lib/models/metadata.js index f86fa9a..19f9e9e 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -2,10 +2,9 @@ import { DataTypes } from "sequelize" export default function (sequelize) { const Metadata = sequelize.define('Metadata', { - id: { + id: {//these will match the igdbid to make things a little easier type: DataTypes.INTEGER, primaryKey: true, - autoIncrement: true }, title: { type: DataTypes.STRING, @@ -15,23 +14,17 @@ export default function (sequelize) { type: DataTypes.STRING }, description: { - type: DataTypes.STRING + type: DataTypes.TEXT }, rating: { type: DataTypes.STRING }, - coverarturl: { + coverartid: { type: DataTypes.STRING }, releasedate: { type: DataTypes.DATE }, - igdbid: { - type: DataTypes.INTEGER - }, - timetobeat: { - type: DataTypes.STRING - }, genre: { type: DataTypes.STRING }, @@ -43,11 +36,14 @@ export default function (sequelize) { }, gamemodes:{ type: DataTypes.STRING + }, + platforms: { + type: DataTypes.STRING } }, { indexes: [ { fields: ['title'] }, - { fields: ['description'] }//If this slows down the db may want to not index this. + { fields: ['description'] },//If this slows down the db may want to not index this. ] }) diff --git a/lib/nonGameTerms.json b/lib/nonGameTerms.json index e5088f4..1259ee0 100644 --- a/lib/nonGameTerms.json +++ b/lib/nonGameTerms.json @@ -7,6 +7,7 @@ "beta", "box", "boxart", + "cbr", "cheat", "config", "cfg", @@ -32,6 +33,7 @@ "mod", "movie", "music", + "mp4", "ost", "overlay", "patch", diff --git a/lib/services/elasticsearch.js b/lib/services/elasticsearch.js index f407793..1f96701 100644 --- a/lib/services/elasticsearch.js +++ b/lib/services/elasticsearch.js @@ -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 "../debugprint.js"; +import { File } from "../models/index.js"; +import { Timer } from "../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,52 @@ 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 }, }); // Create a map of full records by id @@ -187,49 +178,44 @@ 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) => ({ + ...recordMap[hit._id]?.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 out anything that couldn't be found in postgres + results = results.filter(result => result.filename) - // 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 + db: fullRecords, + 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,26 +226,22 @@ 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({ @@ -268,26 +250,26 @@ export async function getSuggestions(query, options) { query: { 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", + }, }, - _source: ['filename', 'category'], - size: 10 - } + _source: ["filename", "category"], + size: 10, + }, }); - return response.hits.hits.map(hit => ({ - suggestion: hit._source.filename + return response.hits.hits.map((hit) => ({ + suggestion: hit._source.filename, })); } 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 +277,18 @@ 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 []; } -} \ No newline at end of file +} diff --git a/lib/time.js b/lib/time.js index 501fa0b..5275d38 100644 --- a/lib/time.js +++ b/lib/time.js @@ -1,4 +1,4 @@ -export class timer { +export class Timer { constructor() { this.startTime = process.hrtime(); } @@ -13,4 +13,7 @@ export class timer { let s = Math.floor(elapsed % 60); return `${h ? h + "h" : ""}${m ? m + "m" : ""}${s + "s"}`; } -} + elapsedSeconds(){ + return this.parseHrtimetoSeconds(process.hrtime(this.startTime)); + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4eed92c..88efe08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" @@ -3175,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", diff --git a/package.json b/package.json index 29d1dba..9e78c8f 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/server.js b/server.js index 4eb3e73..887bfed 100644 --- a/server.js +++ b/server.js @@ -21,6 +21,7 @@ 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 MetadataSearch from "./lib/metadatasearch.js"; let categoryListPath = "./lib/categories.json"; let nonGameTermsPath = "./lib/nonGameTerms.json"; @@ -51,6 +52,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,6 +66,7 @@ for (let field in searchFields) { } let search = new Searcher(searchFields); +let metadataSearch = new MetadataSearch(); async function getFilesJob() { console.log("Updating the file list."); @@ -106,7 +109,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); @@ -193,22 +196,35 @@ app.get("/search", async function (req, res) { if (settings.combineWith != "AND") { delete settings.combineWith; } + settings.pageSize = settings.useOldResults ? 100 : 10; + settings.page = pageNum - 1; let results = await search.findAllMatches(query, settings); debugPrint(results); - if (results.items.length && pageNum == 1) { + let metas = await metadataSearch.getGamesMetadata(results.db); + if (results.count && pageNum == 1) { queryCount += 1; await QueryCount.update({ count: queryCount }, { where: { id: 1 } }); updateDefaults(); } + let resultOutput = []; + for (let x in results.items) { + resultOutput.push({ + file: results.items[x], + metadata: metas[x] || [], + }); + } let options = { query: query, - results: results, + results: resultOutput, + count: results.count, + elapsed: results.elapsed, pageNum: pageNum, + pageCount: Math.ceil(results.count / settings.pageSize), indexing: search.indexing, urlPrefix: urlPrefix, settings: settings, }; - let page = "resultsnew"; + let page = settings.useOldResults ? "resultsold" : "results"; options = buildOptions(page, options); res.render(indexPage, options); }); @@ -511,4 +527,4 @@ if ( await getFilesJob(); } -cron.schedule("0 30 2 * * *", getFilesJob); \ No newline at end of file +cron.schedule("0 30 2 * * *", getFilesJob); diff --git a/views/pages/results.ejs b/views/pages/results.ejs index 74efa8e..c14b3ea 100644 --- a/views/pages/results.ejs +++ b/views/pages/results.ejs @@ -1,12 +1,8 @@ <% - 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 %> @@ -31,7 +27,7 @@

      - <%= __('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) { %> @@ -43,63 +39,11 @@

      -

      <%= __('search.displaying_results', { start: entryStart, end: entryEnd }) %>

      -
      <%= __('results.table.name') %><%= __('results.table.name') %> <%= __('results.table.group') %> <%= __('results.table.category') %> <%= __('results.table.region') %>
      - - - - - - - - - - - <% if (process.env.EMULATOR_ENABLED === 'true') { %> - - <% } %> - - - <% for (let x = entryStart; x < entryEnd; x++) { %> - - - - - - - - - - <% if (process.env.EMULATOR_ENABLED === 'true') { %> - - <% } %> - +
      + <% for (let x = 0; x < results.length; x++) { %> + <%- include("../partials/result", {result: results[x]}) %> <% } %> -
      <%= __('results.table.name') %><%= __('results.table.group') %><%= __('results.table.category') %><%= __('results.table.region') %><%= __('results.table.type') %><%= __('results.table.size') %><%= __('results.table.date') %><%= __('results.table.score') %><%= __('results.table.play') %>
      - - <%= results.items[x].filename %> - - - <%= results.items[x].group %> - - <%= results.items[x].category %> - - <%= results.items[x].region %> - - <%= results.items[x].type %> - - <%= results.items[x].size %> - - <%= results.items[x].date %> - - <%= results.items[x].score.toFixed(2) %> - - <% if (isEmulatorCompatible(results.items[x].category)) { %> - <%= __('emulator.play') %> - <% } else { %> - - <% } %> -
      + <% if(pageCount > 1) { %> @@ -150,33 +94,4 @@ <% } %> - - \ No newline at end of file + \ No newline at end of file diff --git a/views/pages/resultsnew.ejs b/views/pages/resultsold.ejs similarity index 66% rename from views/pages/resultsnew.ejs rename to views/pages/resultsold.ejs index 0bdf09f..74efa8e 100644 --- a/views/pages/resultsnew.ejs +++ b/views/pages/resultsold.ejs @@ -44,11 +44,62 @@

      <%= __('search.displaying_results', { start: entryStart, end: entryEnd }) %>

      -
      + + + + + + + + + + + + <% if (process.env.EMULATOR_ENABLED === 'true') { %> + + <% } %> + + <% for (let x = entryStart; x < entryEnd; x++) { %> - <%- include("../partials/result", {result: results.items[x]}) %> + + + + + + + + + + <% if (process.env.EMULATOR_ENABLED === 'true') { %> + + <% } %> + <% } %> - +
      <%= __('results.table.name') %><%= __('results.table.group') %><%= __('results.table.category') %><%= __('results.table.region') %><%= __('results.table.type') %><%= __('results.table.size') %><%= __('results.table.date') %><%= __('results.table.score') %><%= __('results.table.play') %>
      + + <%= results.items[x].filename %> + + + <%= results.items[x].group %> + + <%= results.items[x].category %> + + <%= results.items[x].region %> + + <%= results.items[x].type %> + + <%= results.items[x].size %> + + <%= results.items[x].date %> + + <%= results.items[x].score.toFixed(2) %> + + <% if (isEmulatorCompatible(results.items[x].category)) { %> + <%= __('emulator.play') %> + <% } else { %> + + <% } %> +
      <% if(pageCount > 1) { %> @@ -99,4 +150,33 @@
      <% } %>
      - \ No newline at end of file + + \ No newline at end of file diff --git a/views/partials/opengraphresults.ejs b/views/partials/opengraphresults.ejs index 622f049..358f974 100644 --- a/views/partials/opengraphresults.ejs +++ b/views/partials/opengraphresults.ejs @@ -1,9 +1,8 @@ <% -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].filename}\n\n` } resultString = resultString.trim() %> diff --git a/views/partials/result.ejs b/views/partials/result.ejs index 68c6d78..552cebc 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -1,14 +1,19 @@ +<% + const metadata = result.metadata || new Object() + const file = result.file || new Object() + const coverUrl = metadata.coverartid ? `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_cover_big/${metadata.coverartid}.webp` : "/public/images/coverart/nocoverart.png" +%>
      - "> +
      -

      <%= result.title || result.filename %>

      -

      Released: <%= result.releaseDate || result.date %> Region: <%= result.region %>

      -

      <%= result.description || "No description was found." %>

      - <% if(result.title) {%> -

      Filename: <%= result.filename %>

      +

      <%= metadata.title || file.filename %>

      +

      Released: <%= metadata.releasedate || file.date %> Region: <%= file.region %> Platform: <%= file.category %>

      +

      <%= metadata.description || "No description was found." %>

      + <% if(metadata.title) {%> +

      Filename: <%= file.filename %>

      <% } %> -

      +

      \ No newline at end of file From 4367b5cdb7b6b729acd5eee708f6e732a6ec4368 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Sat, 24 May 2025 06:39:00 -0600 Subject: [PATCH 17/46] it works and stuff. todo improve search quality improve metadata matching quality --- lib/json/relatedkeywords/names.json | 5 ++- lib/metadatasearch.js | 63 ++++++++++++++++++++++++++--- lib/models/metadata.js | 2 +- lib/nonGameTerms.json | 7 +++- lib/services/elasticsearch.js | 6 ++- server.js | 9 +---- views/pages/results.ejs | 5 ++- views/partials/footer.ejs | 6 --- views/partials/head.ejs | 3 +- views/partials/result.ejs | 34 ++++++++++------ views/public/css/result.css | 42 +++++++++++++++++++ views/public/css/style.css | 28 ------------- views/public/js/utility.js | 6 +++ 13 files changed, 148 insertions(+), 68 deletions(-) create mode 100644 views/public/css/result.css create mode 100644 views/public/js/utility.js diff --git a/lib/json/relatedkeywords/names.json b/lib/json/relatedkeywords/names.json index 920ccf2..dfe4787 100644 --- a/lib/json/relatedkeywords/names.json +++ b/lib/json/relatedkeywords/names.json @@ -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"] ] diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index c339f16..1b89565 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -18,6 +18,7 @@ import { } from "@phalcode/ts-igdb-client"; import { File, Metadata } from "./database.js"; import { Sequelize } from "sequelize"; +import debugPrint from "./debugprint.js"; export default class MetadataSearch { constructor() { @@ -88,11 +89,35 @@ export default class MetadataSearch { .alias(x) .pipe( fields(this.gameFields), - and( - ...this.buildAndClauses("name", "~", query[x].name), - where("game_type.type", "=", "Main Game"), - where("platforms.name", "~", query[x].platform) + or( + and( + ...this.buildAndClauses("name", "~", query[x].name), + where("game_type.type", "!=", "Mod"), + where("game_type.type", "!=", "DLC"), + ...this.buildPlatformClause("~", query[x].platform) + ), + and( + ...this.buildAndClauses( + "alternative_names.name", + "~", + query[x].name + ), + where("game_type.type", "!=", "Mod"), + where("game_type.type", "!=", "DLC"), + ...this.buildPlatformClause("~", query[x].platform) + ), + and( + ...this.buildAndClauses( + "game_localizations.name", + "~", + query[x].name + ), + where("game_type.type", "!=", "Mod"), + where("game_type.type", "!=", "DLC"), + ...this.buildPlatformClause("~", query[x].platform) + ) ), + sort("name", "asc"), limit(1) ) ); @@ -109,10 +134,24 @@ export default class MetadataSearch { return andClauses; } + buildPlatformClause(op, string) { + if (string == "Others") return []; + //special garbage because SOMEONE doesn't value consistency + string = string.replace("Nintendo Wii", "Wii"); + string = string.replace("Nintendo Game Boy", "Game Boy"); + string = string.replace("Sony PlayStation", "Playstation"); + string = string.replace("Microsoft Xbox", "Xbox"); + return [where("platforms.name", op, string, WhereFlags.CONTAINS)]; + } + normalizeName(filename) { if (!filename) return; return filename - .replace(/\.[A-z]{3,3}|\.|&|-|,|v[0-9]+\.[0-9]+|\[.*?\]|\(.*?\)/g, "") + .replace( + /\.[A-z]{3,3}|\.|&|-|\+|,|v[0-9]+\.[0-9]+|\[.*?\]|\(.*?\)|the|usa/gi, + "" + ) + .replace(/\s{2,}/g, " ") .trim(); } @@ -134,6 +173,7 @@ export default class MetadataSearch { } if (!gameQuery.length) return []; let gameMetas = await this.getMetadata(gameQuery); + debugPrint(JSON.stringify(gameMetas, null, 2)); if (!gameMetas.length) return []; for (let x in gameMetas) { if (gameMetas[x].result.length) { @@ -141,10 +181,21 @@ export default class MetadataSearch { gameMetas[x].result[0], games[gameQuery[x].id] ); + } else { + games[x].blockmetadata = true; + games[x].save(); } } let details = await Promise.all(games.map((game) => game.getDetails())); - return details.map((details) => details?.dataValues); + let combined = []; + //make sure the metadata gets included with the gamedata + for (let x in games) { + combined.push({ + file: games[x].dataValues, + metadata: details[x]?.dataValues, + }); + } + return combined; } catch (error) { console.error("Error getting metadata:", error); } diff --git a/lib/models/metadata.js b/lib/models/metadata.js index 19f9e9e..7cc42f4 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -23,7 +23,7 @@ export default function (sequelize) { type: DataTypes.STRING }, releasedate: { - type: DataTypes.DATE + type: DataTypes.DATEONLY }, genre: { type: DataTypes.STRING diff --git a/lib/nonGameTerms.json b/lib/nonGameTerms.json index 1259ee0..926b2a4 100644 --- a/lib/nonGameTerms.json +++ b/lib/nonGameTerms.json @@ -1,6 +1,7 @@ { "terms": [ "7z", + "action replay", "addon", "artwork", "audio", @@ -8,10 +9,13 @@ "box", "boxart", "cbr", + "chd", "cheat", "config", + "codebreaker", "cfg", "csv", + "datel", "debug", "dlc", "document", @@ -22,11 +26,9 @@ "figurine", "firmware", "guide", - "hack", "html", "ini", "installer", - "intro", "json", "jpg", "manual", @@ -49,6 +51,7 @@ "setup", "soundtrack", "sqlite", + "swf", "terms", "tool", "trainer", diff --git a/lib/services/elasticsearch.js b/lib/services/elasticsearch.js index 1f96701..a46f50e 100644 --- a/lib/services/elasticsearch.js +++ b/lib/services/elasticsearch.js @@ -179,13 +179,15 @@ export async function search(query, options) { // Build results with full PostgreSQL records let results = response.hits.hits.map((hit) => ({ - ...recordMap[hit._id]?.dataValues, + file:{ + ...recordMap[hit._id]?.dataValues, + }, score: hit._score, highlights: hit.highlight, })); //Filter out anything that couldn't be found in postgres - results = results.filter(result => result.filename) + results = results.filter(result => result.file.filename) const elapsed = timer.elapsedSeconds(); return { diff --git a/server.js b/server.js index 887bfed..c9b4401 100644 --- a/server.js +++ b/server.js @@ -206,16 +206,9 @@ app.get("/search", async function (req, res) { await QueryCount.update({ count: queryCount }, { where: { id: 1 } }); updateDefaults(); } - let resultOutput = []; - for (let x in results.items) { - resultOutput.push({ - file: results.items[x], - metadata: metas[x] || [], - }); - } let options = { query: query, - results: resultOutput, + results: metas?.length ? metas : results.items, count: results.count, elapsed: results.elapsed, pageNum: pageNum, diff --git a/views/pages/results.ejs b/views/pages/results.ejs index c14b3ea..83151ae 100644 --- a/views/pages/results.ejs +++ b/views/pages/results.ejs @@ -10,6 +10,7 @@ +
      @@ -81,7 +82,9 @@
    • class="page-link previous" aria-controls="results" aria-disabled="true" aria-label="Previous" data-dt-idx="previous" tabindex="-1">‹
    • 1
    • <%- 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; + %>
    • <%= x %>
    • <% } %> <%- pageNum <= pageCount - 5 ? ellipsesElem : '' %> diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs index c606278..4d7a7ce 100644 --- a/views/partials/footer.ejs +++ b/views/partials/footer.ejs @@ -9,12 +9,6 @@
      - \ No newline at end of file + + \ No newline at end of file diff --git a/views/partials/result.ejs b/views/partials/result.ejs index 552cebc..77a5677 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -4,16 +4,26 @@ const coverUrl = metadata.coverartid ? `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_cover_big/${metadata.coverartid}.webp` : "/public/images/coverart/nocoverart.png" %>
      -
      - -
      -
      -

      <%= metadata.title || file.filename %>

      -

      Released: <%= metadata.releasedate || file.date %> Region: <%= file.region %> Platform: <%= file.category %>

      -

      <%= metadata.description || "No description was found." %>

      - <% if(metadata.title) {%> -

      Filename: <%= file.filename %>

      - <% } %> -

      -

      +
      + +
      +
      +

      <%= metadata.title || file.filename %>

      +

      Released: <%= metadata.releasedate || file.date %> + Region: <%= file.region %> + Platform: <%= file.category %> + <% if(metadata.genre){ %> + Genres: <%= JSON.parse(metadata.genre).join(' / ') %> + <% } %> +

      +

      <%= metadata.description || "No description was found."//todo: localize %>

      + <% if(metadata.title) {%> +

      Filename: <%= file.filename %>

      + <% } %> +

      Release Group: <%= file.group %>

      +

      + More Info + Download + Play In Browser +

      \ No newline at end of file diff --git a/views/public/css/result.css b/views/public/css/result.css new file mode 100644 index 0000000..2788683 --- /dev/null +++ b/views/public/css/result.css @@ -0,0 +1,42 @@ +.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; +} +.title a { + font-weight: bold; + color: #f0a400; +} +.info { + font-size: 0.8em; +} +.file { + font-size: 0.8em; +} +.group { + font-size: 0.8em; +} +.infoitem{ + margin-right: 0; +} +.cover{ + margin-left: 1rem; +} \ No newline at end of file diff --git a/views/public/css/style.css b/views/public/css/style.css index 0795531..81167f4 100644 --- a/views/public/css/style.css +++ b/views/public/css/style.css @@ -184,31 +184,3 @@ thead > tr > th.dt-orderable span.dt-column-order.order-desc::before{ thead > tr > th.dt-orderable span.dt-column-order.order-asc::after{ opacity: 1; } -.coverart{ - object-fit: contain; - height: 200px; - width: 150px; -} -.description{ - white-space: normal; - display: -webkit-box; - -webkit-line-clamp: 4; - -webkit-box-orient: vertical; - overflow: hidden; -} -.searchresult { - padding: 10px; -} -.title { - font-weight: bold; - color: #f0a400; -} -.info { - font-size: 0.8em; -} -.file { - font-size: 0.8em; -} -.infoitem{ - margin-right: 1rem; -} \ No newline at end of file diff --git a/views/public/js/utility.js b/views/public/js/utility.js new file mode 100644 index 0000000..41f608a --- /dev/null +++ b/views/public/js/utility.js @@ -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) + } \ No newline at end of file From 91e5feefc61824b9365cb4dd9d692d41a82b7ffe Mon Sep 17 00:00:00 2001 From: Alexandra Date: Mon, 26 May 2025 07:56:05 -0600 Subject: [PATCH 18/46] implement task queue for metadata retrieval to respect igdb limits --- lib/metadatasearch.js | 22 ++++++++---- lib/taskqueue.js | 81 +++++++++++++++++++++++++++++++++++++++++++ server.js | 2 +- 3 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 lib/taskqueue.js diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 1b89565..610ff13 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -17,8 +17,8 @@ import { offset, } from "@phalcode/ts-igdb-client"; import { File, Metadata } from "./database.js"; -import { Sequelize } from "sequelize"; import debugPrint from "./debugprint.js"; +import TaskQueue from "./taskqueue.js"; export default class MetadataSearch { constructor() { @@ -27,6 +27,7 @@ export default class MetadataSearch { client_secret: process.env.TWITCH_CLIENT_SECRET, }; this.setupClient(); + this.queue = new TaskQueue(); } gameFields = [ "name", @@ -139,7 +140,8 @@ export default class MetadataSearch { //special garbage because SOMEONE doesn't value consistency string = string.replace("Nintendo Wii", "Wii"); string = string.replace("Nintendo Game Boy", "Game Boy"); - string = string.replace("Sony PlayStation", "Playstation"); + string = string.replace("Nintendo Satellaview", "Satellaview"); + string = string.replace("Sony PlayStation", "PlayStation"); string = string.replace("Microsoft Xbox", "Xbox"); return [where("platforms.name", op, string, WhereFlags.CONTAINS)]; } @@ -157,7 +159,7 @@ export default class MetadataSearch { async getGamesMetadata(games) { try { - if (!this.authorized || !games.length) return []; + if (!this.authorized || !games.length) return; let gameQuery = []; for (let x in games) { if (!(await games[x].getDetails())) @@ -171,10 +173,10 @@ export default class MetadataSearch { } } } - if (!gameQuery.length) return []; - let gameMetas = await this.getMetadata(gameQuery); + if (!gameQuery.length) return; + let gameMetas = await this.queue.enqueue(this.getMetadata, this, gameQuery) debugPrint(JSON.stringify(gameMetas, null, 2)); - if (!gameMetas.length) return []; + if (!gameMetas.length) return; for (let x in gameMetas) { if (gameMetas[x].result.length) { await this.addMetadataToDb( @@ -186,6 +188,14 @@ export default class MetadataSearch { games[x].save(); } } + } catch (error) { + console.error("Error getting metadata:", error); + } + } + + async queueGetGamesMetadata(games) { + try { + await this.getGamesMetadata(games); //we don't actually care as long as it finishes let details = await Promise.all(games.map((game) => game.getDetails())); let combined = []; //make sure the metadata gets included with the gamedata diff --git a/lib/taskqueue.js b/lib/taskqueue.js new file mode 100644 index 0000000..49d70a0 --- /dev/null +++ b/lib/taskqueue.js @@ -0,0 +1,81 @@ +export default class TaskQueue { + constructor(maxTasksPerSecond = 4, maxSimultaneousTasks = 8) { + this.maxTasksPerSecond = maxTasksPerSecond; + 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) => { + 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, + }; + } +} diff --git a/server.js b/server.js index c9b4401..cdf3de0 100644 --- a/server.js +++ b/server.js @@ -200,7 +200,7 @@ app.get("/search", async function (req, res) { settings.page = pageNum - 1; let results = await search.findAllMatches(query, settings); debugPrint(results); - let metas = await metadataSearch.getGamesMetadata(results.db); + let metas = await metadataSearch.queueGetGamesMetadata(results.db); if (results.count && pageNum == 1) { queryCount += 1; await QueryCount.update({ count: queryCount }, { where: { id: 1 } }); From e94a21efccca9a298bf2b91f5be7bf4c60e1a084 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Mon, 26 May 2025 14:31:12 -0600 Subject: [PATCH 19/46] Fixed up old table search Added a toggle for table search added more info page god knows what else --- config/locales/en.json | 15 ++++- lib/metadatasearch.js | 13 ++-- lib/models/metadata.js | 6 ++ lib/services/elasticsearch.js | 8 +-- lib/taskqueue.js | 13 +++- server.js | 35 ++++++++++- views/pages/about.ejs | 5 ++ views/pages/info.ejs | 99 ++++++++++++++++++++++++++++++ views/pages/results.ejs | 6 -- views/pages/resultsold.ejs | 104 +++++++++++++++----------------- views/pages/settings.ejs | 6 +- views/partials/carousel.ejs | 45 ++++++++++++++ views/partials/result.ejs | 24 +++++--- views/public/css/info.css | 46 ++++++++++++++ views/public/css/resultsold.css | 42 +++++++++++++ views/public/css/style.css | 40 ------------ views/public/js/settings.js | 3 + views/public/js/video.js | 32 ++++++++++ 18 files changed, 420 insertions(+), 122 deletions(-) create mode 100644 views/pages/info.ejs create mode 100644 views/partials/carousel.ejs create mode 100644 views/public/css/info.css create mode 100644 views/public/css/resultsold.css create mode 100644 views/public/js/video.js diff --git a/config/locales/en.json b/config/locales/en.json index b51bb02..d878da2 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -21,7 +21,12 @@ "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", + "region": "Region", + "platform": "Platform" }, "about": { "title": "About", @@ -40,6 +45,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}}. This information is pulled on search. Search queries may be slow until metadata has been cached in the database." } }, "settings": { @@ -69,6 +78,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": "Redirects your search to the old table formatted results. These will load faster as they will ignore pulling metadata." } }, "save": "Save Settings" diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 610ff13..8f9b434 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -48,6 +48,8 @@ export default class MetadataSearch { "game_localizations.region.name", "platforms.name", "game_type.type", + "screenshots.image_id", + "videos.video_id" ]; async setupClient() { @@ -143,6 +145,8 @@ export default class MetadataSearch { string = string.replace("Nintendo Satellaview", "Satellaview"); string = string.replace("Sony PlayStation", "PlayStation"); string = string.replace("Microsoft Xbox", "Xbox"); + string = string.replace("Commodore 64", "Commodore C64"); + string = string.replace("Commodore Amiga", "Amiga"); return [where("platforms.name", op, string, WhereFlags.CONTAINS)]; } @@ -208,16 +212,13 @@ export default class MetadataSearch { return combined; } catch (error) { console.error("Error getting metadata:", error); + return [] } } async addMetadataToDb(metadata, game) { try { - let md = await Metadata.findOne({ - where: { - id: metadata.id, - }, - }); + let md = await Metadata.findByPk(metadata.id); if (!md) { md = await Metadata.build( { @@ -237,6 +238,8 @@ export default class MetadataSearch { platforms: JSON.stringify( metadata.platforms?.map((platform) => platform.name) ), + screenshots: JSON.stringify(metadata.screenshots?.map((ss) => ss.image_id)), + videos: JSON.stringify(metadata.videos?.map((v) => v.video_id)) }, { returning: true, diff --git a/lib/models/metadata.js b/lib/models/metadata.js index 7cc42f4..cfeaded 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -39,6 +39,12 @@ export default function (sequelize) { }, platforms: { type: DataTypes.STRING + }, + screenshots: { + type: DataTypes.STRING + }, + videos:{ + type: DataTypes.STRING } }, { indexes: [ diff --git a/lib/services/elasticsearch.js b/lib/services/elasticsearch.js index a46f50e..f63858f 100644 --- a/lib/services/elasticsearch.js +++ b/lib/services/elasticsearch.js @@ -102,7 +102,7 @@ export async function bulkIndexFiles(files) { filenamekws: file.filenamekws, categorykws: file.categorykws, regionkws: file.regionkws, - nongame: file.nongame + nongame: file.nongame, }, ]); @@ -168,7 +168,7 @@ export async function search(query, options) { // Fetch full records from PostgreSQL for the search results const ids = response.hits.hits.map((hit) => hit._id); const fullRecords = await File.findAll({ - where: { id: ids }, + where: { id: ids } }); // Create a map of full records by id @@ -179,7 +179,7 @@ export async function search(query, options) { // Build results with full PostgreSQL records let results = response.hits.hits.map((hit) => ({ - file:{ + file: { ...recordMap[hit._id]?.dataValues, }, score: hit._score, @@ -187,7 +187,7 @@ export async function search(query, options) { })); //Filter out anything that couldn't be found in postgres - results = results.filter(result => result.file.filename) + results = results.filter((result) => result.file.filename); const elapsed = timer.elapsedSeconds(); return { diff --git a/lib/taskqueue.js b/lib/taskqueue.js index 49d70a0..98664af 100644 --- a/lib/taskqueue.js +++ b/lib/taskqueue.js @@ -1,6 +1,11 @@ export default class TaskQueue { - constructor(maxTasksPerSecond = 4, maxSimultaneousTasks = 8) { + constructor( + maxTasksPerSecond = 4, + maxSimultaneousTasks = 8, + maxQueueLength = 20 + ) { this.maxTasksPerSecond = maxTasksPerSecond; + this.maxQueueLength = maxQueueLength; this.maxSimultaneousTasks = maxSimultaneousTasks; this.queue = []; this.processing = false; @@ -9,8 +14,12 @@ export default class TaskQueue { this.tasksWaiting = 0; } - async enqueue(taskFunction, that=this, ...args) { + 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, diff --git a/server.js b/server.js index cdf3de0..a8acf59 100644 --- a/server.js +++ b/server.js @@ -171,7 +171,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 { @@ -198,9 +202,13 @@ app.get("/search", async function (req, res) { } settings.pageSize = settings.useOldResults ? 100 : 10; settings.page = pageNum - 1; + settings.sort = req.query.o || ""; let results = await search.findAllMatches(query, settings); debugPrint(results); - let metas = await metadataSearch.queueGetGamesMetadata(results.db); + let metas = []; + if (!settings.useOldResults) { + metas = await metadataSearch.queueGetGamesMetadata(results.db); + } if (results.count && pageNum == 1) { queryCount += 1; await QueryCount.update({ count: queryCount }, { where: { id: 1 } }); @@ -301,6 +309,29 @@ 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"); + if (!metadataSearch.authorized) { + res.redirect("/"); + return; + } + let romId = parseInt(req.params.id); + let romFile = await search.findIndex(romId); + let romInfo = await metadataSearch.queueGetGamesMetadata([romFile]); + + if (!romInfo.length) { + res.redirect("/"); + return; + } + let options = { + romFile: romInfo[0], + }; + 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") { diff --git a/views/pages/about.ejs b/views/pages/about.ejs index 2084c88..7961ed5 100644 --- a/views/pages/about.ejs +++ b/views/pages/about.ejs @@ -16,6 +16,11 @@ <%= __('about.donate') %>
      +
      +
      <%= __('about.metadata.title') %>
      +

      <%= __('about.metadata.description', {metadata_source: 'IGDB'}) %>

      +
      +
      <%= __('about.emulator.title') %>
      <% if (process.env.EMULATOR_ENABLED === 'true') { %> diff --git a/views/pages/info.ejs b/views/pages/info.ejs new file mode 100644 index 0000000..52975f9 --- /dev/null +++ b/views/pages/info.ejs @@ -0,0 +1,99 @@ +<% + const metadata = romFile.metadata || new Object() + const file = romFile.file || new Object() + const coverUrl = metadata.coverartid ? `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_cover_big/${metadata.coverartid}.webp` : "/public/images/coverart/nocoverart.png" + let images = [] + if(metadata.screenshots){ + images = JSON.parse(metadata.screenshots).map((im) => `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_720p/${im}.webp`) + } + let videos = [] + if(metadata.videos){ + videos = JSON.parse(metadata.videos) + } +%> + +
      +
      +
      +
      +

      <%= metadata.title %>

      +

      <%= file.category %>

      +
      +
      + +
      + <% if(metadata.rating) {%> +
      + <% + const fullstars = Math.floor(metadata.rating / 20) + const halfstars = Math.floor(metadata.rating % 20 / 10) + const nostars = 5 - fullstars - halfstars + const nostarstring = '' + const fullstarstring = '' + const halfstarstring = '' + let stars = '' + for(let x = 0; x < fullstars ; x++){ + stars += fullstarstring + } + if(halfstars){ + stars += halfstarstring + } + for(let x = 0; x < nostars; x++){ + stars += nostarstring + } + %> +

      <%- stars%> (<%= Math.floor(metadata.rating) %>%)

      +
      + <% } %> + <% if(metadata.developers) {%> +
      +

      Developed by: <%= JSON.parse(metadata.developers).join(", ") %>

      +
      + <% } %> + <% if(metadata.publishers) {%> +
      +

      Published by: <%= JSON.parse(metadata.publishers).join(", ") %>

      +
      + <% } %> + <% if(metadata.releasedate) {%> +
      +

      Release date: <%= metadata.releasedate %>

      +
      + <% } %> + <% if(file.region) {%> +
      +

      Region: <%= file.region %>

      +
      + <% } %> + <% if(metadata.genre) {%> +
      +

      Genre: <%= JSON.parse(metadata.genre).join(", ") %>

      +
      + <% } %> + <% if(metadata.gamemodes) {%> +
      +

      Gameplay modes: <%= JSON.parse(metadata.gamemodes).join(", ") %>

      +
      + <% } %> +
      +

      <%= metadata.description %>

      +
      +
      +

      + Download + <% if (process.env.EMULATOR_ENABLED === 'true') { %> + <% if (isEmulatorCompatible(file.category)) { %> + <%= __('emulator.play')%> <% } else { %> + + <% } + }%> +

      +
      +
      +
      + <%- include("../partials/carousel", {images: images, videos: videos})%> +
      +
      +
      + +
      \ No newline at end of file diff --git a/views/pages/results.ejs b/views/pages/results.ejs index 83151ae..3b3218c 100644 --- a/views/pages/results.ejs +++ b/views/pages/results.ejs @@ -4,12 +4,6 @@ pageNum = 1 } %> - - - - - -
      diff --git a/views/pages/resultsold.ejs b/views/pages/resultsold.ejs index 74efa8e..a436ef2 100644 --- a/views/pages/resultsold.ejs +++ b/views/pages/resultsold.ejs @@ -1,12 +1,8 @@ <% - 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 %> @@ -14,6 +10,7 @@ +
      @@ -31,7 +28,7 @@

        - <%= __('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) { %> @@ -43,55 +40,55 @@

        -

        <%= __('search.displaying_results', { start: entryStart, end: entryEnd }) %>

        +

        <%= __('search.displaying_results', { start: ((pageNum - 1) * 100), end: pageNum * 100 < count ? pageNum * 100 : count }) %>

        - - - - - - - - + + + + + + + + <% if (process.env.EMULATOR_ENABLED === 'true') { %> <% } %> - <% for (let x = entryStart; x < entryEnd; x++) { %> + <% for (let x = 0; x < results.length; x++) { %> <% if (process.env.EMULATOR_ENABLED === 'true') { %> <% } %> diff --git a/views/pages/search.ejs b/views/pages/search.ejs index 227c3b8..e544eb3 100644 --- a/views/pages/search.ejs +++ b/views/pages/search.ejs @@ -7,6 +7,7 @@
        +
          diff --git a/views/pages/settings.ejs b/views/pages/settings.ejs index 17959cf..37be852 100644 --- a/views/pages/settings.ejs +++ b/views/pages/settings.ejs @@ -1,6 +1,3 @@ - - -
          diff --git a/views/partials/result.ejs b/views/partials/result.ejs
          index ac17b6b..ba46e6d 100644
          --- a/views/partials/result.ejs
          +++ b/views/partials/result.ejs
          @@ -31,7 +31,7 @@
                 <% if (process.env.EMULATOR_ENABLED === 'true') { %>
                 <% if (isEmulatorCompatible(file.category)) { %>
                 <%= __('emulator.play')%> <% } else { %>
          -      
          +      
                 <% } 
               }%>
           
          diff --git a/views/public/js/navbar.js b/views/public/js/navbar.js
          index ff5ffdb..e9f1a99 100644
          --- a/views/public/js/navbar.js
          +++ b/views/public/js/navbar.js
          @@ -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')
          diff --git a/views/public/js/settings.js b/views/public/js/settings.js
          index 61ca2b8..871cedd 100644
          --- a/views/public/js/settings.js
          +++ b/views/public/js/settings.js
          @@ -58,9 +58,6 @@
             }
           
             function loadSettings(){
          -      $(function () {
          -        $('[data-toggle="tooltip"]').tooltip()
          -      })
               if(!settingStore) {
                 settings = structuredClone(defaults)
                 settingStore = JSON.stringify(settings)
          
          From 071184c2abf0870647e3493570e844b32b5dea17 Mon Sep 17 00:00:00 2001
          From: Alexandra 
          Date: Tue, 27 May 2025 20:34:38 -0600
          Subject: [PATCH 24/46] fix opengraph fix new results pulling too many results
          
          ---
           server.js                           | 2 +-
           views/partials/opengraphresults.ejs | 2 +-
           2 files changed, 2 insertions(+), 2 deletions(-)
          
          diff --git a/server.js b/server.js
          index 60f42a8..ad49919 100644
          --- a/server.js
          +++ b/server.js
          @@ -201,7 +201,7 @@ app.get("/search", async function (req, res) {
               delete settings.combineWith;
             }
             let loadOldResults = req.query.old === "true" ? true : false
          -  settings.pageSize = settings.useOldResults ? 100 : 10;
          +  settings.pageSize = loadOldResults ? 100 : 10;
             settings.page = pageNum - 1;
             settings.sort = req.query.o || "";
             let results = await search.findAllMatches(query, settings);
          diff --git a/views/partials/opengraphresults.ejs b/views/partials/opengraphresults.ejs
          index 358f974..74c20f4 100644
          --- a/views/partials/opengraphresults.ejs
          +++ b/views/partials/opengraphresults.ejs
          @@ -2,7 +2,7 @@
           let length = results.length > 5 ? 5 : results.length
           let resultString = ''
           for(let x = 0 ; x < length; x++){
          -    resultString += `${x + 1}: ${results[x].filename}\n\n`
          +    resultString += `${x + 1}: ${results[x].file.filename}\n\n`
           }
           resultString = resultString.trim()
           %>
          
          From c3df623b2b1aabd130443342f9d07690806804b2 Mon Sep 17 00:00:00 2001
          From: Alexandra 
          Date: Tue, 27 May 2025 21:01:30 -0600
          Subject: [PATCH 25/46] cleanup disable new search results when igdb not
           authorized
          
          ---
           server.js                                   |   3 +-
           views/pages/settings.ejs                    |   2 +-
           views/public/images/coverart/testcover.webp | Bin 10098 -> 0 bytes
           views/public/js/video.js                    |  32 --------------------
           4 files changed, 3 insertions(+), 34 deletions(-)
           delete mode 100644 views/public/images/coverart/testcover.webp
           delete mode 100644 views/public/js/video.js
          
          diff --git a/server.js b/server.js
          index ad49919..0d51271 100644
          --- a/server.js
          +++ b/server.js
          @@ -200,7 +200,7 @@ app.get("/search", async function (req, res) {
             if (settings.combineWith != "AND") {
               delete settings.combineWith;
             }
          -  let loadOldResults = req.query.old === "true" ? true : false
          +  let loadOldResults = req.query.old === "true" || !metadataSearch.authorized ? true : false
             settings.pageSize = loadOldResults ? 100 : 10;
             settings.page = pageNum - 1;
             settings.sort = req.query.o || "";
          @@ -259,6 +259,7 @@ app.get("/lucky", async function (req, res) {
           app.get("/settings", function (req, res) {
             let options = { defaultSettings: defaultSettings };
             let page = "settings";
          +  options.oldSettingsAvailable = metadataSearch.authorized
             options = buildOptions(page, options);
             res.render(indexPage, options);
           });
          diff --git a/views/pages/settings.ejs b/views/pages/settings.ejs
          index 37be852..8729493 100644
          --- a/views/pages/settings.ejs
          +++ b/views/pages/settings.ejs
          @@ -51,7 +51,7 @@
                           <%= __('settings.extras.hide_non_game.label') %> 
                         
                         
                       
          diff --git a/views/public/images/coverart/testcover.webp b/views/public/images/coverart/testcover.webp deleted file mode 100644 index c2b3f5fbc0e8764464f75985ba00cc6ca1c678f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10098 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 { - // 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, - }, - }); -} \ No newline at end of file From 8538dfadcc7112c999cbdf5a2528eb6dc4aaed28 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Tue, 27 May 2025 22:35:11 -0600 Subject: [PATCH 26/46] add load spinner add flag icons add console icons --- lib/consoleicons.js | 22 ++++++++++++++++++++ lib/flag.js | 41 +++++++++++++++++++++++++++++++++++++ server.js | 13 ++++++++++-- views/pages/info.ejs | 4 ++-- views/pages/results.ejs | 11 +++++++--- views/pages/resultsold.ejs | 7 ++++++- views/pages/search.ejs | 11 +++++++--- views/partials/result.ejs | 8 ++++---- views/public/css/info.css | 7 +++++++ views/public/css/result.css | 17 ++++++++++++++- views/public/css/style.css | 15 ++++++++++++++ views/public/js/video.js | 32 +++++++++++++++++++++++++++++ 12 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 lib/consoleicons.js create mode 100644 lib/flag.js create mode 100644 views/public/js/video.js diff --git a/lib/consoleicons.js b/lib/consoleicons.js new file mode 100644 index 0000000..74c99f1 --- /dev/null +++ b/lib/consoleicons.js @@ -0,0 +1,22 @@ +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') + if(this.ifConsoleExists(console)){ + return `` + } + return '' + } +} \ No newline at end of file diff --git a/lib/flag.js b/lib/flag.js new file mode 100644 index 0000000..9ff6b55 --- /dev/null +++ b/lib/flag.js @@ -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 `` + } + return '' + } +} diff --git a/server.js b/server.js index 0d51271..c141caf 100644 --- a/server.js +++ b/server.js @@ -22,6 +22,8 @@ import i18n, { locales } from "./config/i18n.js"; import { v4 as uuidv4 } from "uuid"; import { optimizeDatabaseKws } from "./lib/dboptimize.js"; import MetadataSearch from "./lib/metadatasearch.js"; +import Flag from "./lib/flag.js"; +import ConsoleIcons from "./lib/consoleicons.js"; let categoryListPath = "./lib/categories.json"; let nonGameTermsPath = "./lib/nonGameTerms.json"; @@ -33,6 +35,8 @@ let crawlTime = 0; let queryCount = 0; let fileCount = 0; let indexPage = "pages/index"; +let flags = new Flag(); +let consoleIcons = new ConsoleIcons(emulatorsData); // Initialize databases await initDB(); @@ -200,7 +204,8 @@ app.get("/search", async function (req, res) { if (settings.combineWith != "AND") { delete settings.combineWith; } - let loadOldResults = req.query.old === "true" || !metadataSearch.authorized ? true : false + let loadOldResults = + req.query.old === "true" || !metadataSearch.authorized ? true : false; settings.pageSize = loadOldResults ? 100 : 10; settings.page = pageNum - 1; settings.sort = req.query.o || ""; @@ -225,6 +230,8 @@ app.get("/search", async function (req, res) { indexing: search.indexing, urlPrefix: urlPrefix, settings: settings, + flags: flags, + consoleIcons: consoleIcons }; let page = loadOldResults ? "resultsold" : "results"; options = buildOptions(page, options); @@ -259,7 +266,7 @@ app.get("/lucky", async function (req, res) { app.get("/settings", function (req, res) { let options = { defaultSettings: defaultSettings }; let page = "settings"; - options.oldSettingsAvailable = metadataSearch.authorized + options.oldSettingsAvailable = metadataSearch.authorized; options = buildOptions(page, options); res.render(indexPage, options); }); @@ -328,6 +335,8 @@ app.get("/info/:id", async function (req, res) { } let options = { romFile: romInfo[0], + flags: flags, + consoleIcons: consoleIcons }; let page = "info"; options = buildOptions(page, options); diff --git a/views/pages/info.ejs b/views/pages/info.ejs index d8e3844..f6a0554 100644 --- a/views/pages/info.ejs +++ b/views/pages/info.ejs @@ -17,7 +17,7 @@

          <%= metadata.title %>

          -

          <%= file.category %>

          +

          <%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %>

          @@ -62,7 +62,7 @@ <% } %> <% if(file.region) {%>
          -

          <%= __('search.region') %> <%= file.region %>

          +

          <%= __('search.region') %> <%= file.region %> <%- flags.createFlag(file.region) %>

          <% } %> <% if(metadata.genre) {%> diff --git a/views/pages/results.ejs b/views/pages/results.ejs index ab4ad5d..0a80e96 100644 --- a/views/pages/results.ejs +++ b/views/pages/results.ejs @@ -6,7 +6,7 @@ %>
          - +
          @@ -16,7 +16,7 @@ - +
            @@ -91,4 +91,9 @@
            <% } %>
            -
            \ No newline at end of file +
            + \ No newline at end of file diff --git a/views/pages/resultsold.ejs b/views/pages/resultsold.ejs index 43c0de9..b298a6a 100644 --- a/views/pages/resultsold.ejs +++ b/views/pages/resultsold.ejs @@ -22,7 +22,7 @@ - +
              @@ -175,4 +175,9 @@ // window.location = location.protocol + '//' + location.host + location.pathname + '?' + URLParams.toString() // }) // }) + + \ No newline at end of file diff --git a/views/pages/search.ejs b/views/pages/search.ejs index e544eb3..a315202 100644 --- a/views/pages/search.ejs +++ b/views/pages/search.ejs @@ -5,17 +5,22 @@ <%= __('nav.search') %>!
              - +
                - +
                - \ No newline at end of file + + \ No newline at end of file diff --git a/views/partials/result.ejs b/views/partials/result.ejs index ba46e6d..45db1da 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -8,10 +8,10 @@
                -

                <%= metadata.title || file.filename %>

                -

                <%= __('search.released') %>: <%= metadata.releasedate || file.date %> - <%= __('search.region') %> <%= file.region %> - <%= __('search.platform') %> <%= file.category %> +

                <%= metadata.title || file.filename %>

                +

                <%= __('search.released') %> <%= metadata.releasedate || file.date %> + <%= __('search.region') %> <%= file.region %> <%- flags.createFlag(file.region) %> + <%= __('search.platform') %> <%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %> <% if(metadata.genre){ %> <%= __('search.genre') %> <%= JSON.parse(metadata.genre).join(' / ') %> <% } %> diff --git a/views/public/css/info.css b/views/public/css/info.css index 1cc0c99..91a95f7 100644 --- a/views/public/css/info.css +++ b/views/public/css/info.css @@ -43,4 +43,11 @@ object-fit: contain; height: auto; width: 240px; +} + +.console { + height: 1.5rem; +} +.text-secondary{ + font-size: 1.5rem; } \ No newline at end of file diff --git a/views/public/css/result.css b/views/public/css/result.css index 2788683..4794632 100644 --- a/views/public/css/result.css +++ b/views/public/css/result.css @@ -20,13 +20,14 @@ font-weight: bold; color: #f0a400; margin-bottom: 0!important; + font-size: 1.25em; } .title a { font-weight: bold; color: #f0a400; } .info { - font-size: 0.8em; + font-size: 1em; } .file { font-size: 0.8em; @@ -36,7 +37,21 @@ } .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; } \ No newline at end of file diff --git a/views/public/css/style.css b/views/public/css/style.css index 136009f..e9865f3 100644 --- a/views/public/css/style.css +++ b/views/public/css/style.css @@ -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; +} \ No newline at end of file diff --git a/views/public/js/video.js b/views/public/js/video.js new file mode 100644 index 0000000..9b5371a --- /dev/null +++ b/views/public/js/video.js @@ -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, + }, + }); +} \ No newline at end of file From cefa1f6a358dedc27c7424e3b332ad50664d74d9 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Wed, 28 May 2025 01:06:58 -0600 Subject: [PATCH 27/46] design tweaks fix to come platforms not showing consoles --- lib/consoleicons.js | 2 ++ views/pages/info.ejs | 2 +- views/public/css/info.css | 31 +++++++++++++++++++++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/consoleicons.js b/lib/consoleicons.js index 74c99f1..7b48f29 100644 --- a/lib/consoleicons.js +++ b/lib/consoleicons.js @@ -14,6 +14,8 @@ export default class ConsoleIcons { 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 `` } diff --git a/views/pages/info.ejs b/views/pages/info.ejs index f6a0554..69d13a6 100644 --- a/views/pages/info.ejs +++ b/views/pages/info.ejs @@ -17,7 +17,7 @@

                <%= metadata.title %>

                -

                <%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %>

                +

                <%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %>

                diff --git a/views/public/css/info.css b/views/public/css/info.css index 91a95f7..c2511d1 100644 --- a/views/public/css/info.css +++ b/views/public/css/info.css @@ -12,12 +12,13 @@ left: 0; width: 100%; height: 100%; + vertical-align: middle; + align-items: center; } .embed-responsive { - position: relative; + position: initial; overflow: hidden; width: 100%; - } .slideshow-container { position: relative; @@ -45,9 +46,27 @@ width: 240px; } -.console { - height: 1.5rem; +.carousel-inner { + height: 0; + padding-bottom: 60%; } -.text-secondary{ - font-size: 1.5rem; + +.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; } \ No newline at end of file From c231f7ffc952fb0ee9fe8e060bd49d995df2c814 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 29 May 2025 10:10:34 -0600 Subject: [PATCH 28/46] redo metadata, opting to pull everything in at once. --- lib/debugprint.js | 10 ++ lib/dircrawl.js | 30 ++-- lib/json/igdb_platform_map.json | 94 +++++++++++ lib/metadatasearch.js | 287 ++++++++++++++------------------ lib/models/metadata.js | 157 +++++++++++------ 5 files changed, 353 insertions(+), 225 deletions(-) create mode 100644 lib/json/igdb_platform_map.json diff --git a/lib/debugprint.js b/lib/debugprint.js index af920ba..a79c937 100644 --- a/lib/debugprint.js +++ b/lib/debugprint.js @@ -8,4 +8,14 @@ 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); + } } \ No newline at end of file diff --git a/lib/dircrawl.js b/lib/dircrawl.js index 4ac9809..caca3d6 100644 --- a/lib/dircrawl.js +++ b/lib/dircrawl.js @@ -2,8 +2,8 @@ import { getTableRows, parseOutFile } from "./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 { File } from "./models/index.js"; +import { bulkIndexFiles } from "./services/elasticsearch.js"; import { optimizeDatabaseKws } from "./dboptimize.js"; import { Timer } from "./time.js"; @@ -15,7 +15,7 @@ let piscina = new Piscina({ 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 +94,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 +124,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) { @@ -153,7 +157,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, @@ -162,20 +166,24 @@ async function processBatch(files) { date: file.date, region: file.region, group: file.group, - nongame: file.nongame + 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); } } @@ -205,4 +213,4 @@ function singleLineStatus(str) { } else { console.log(str); } -} \ No newline at end of file +} diff --git a/lib/json/igdb_platform_map.json b/lib/json/igdb_platform_map.json new file mode 100644 index 0000000..1df4aa7 --- /dev/null +++ b/lib/json/igdb_platform_map.json @@ -0,0 +1,94 @@ +{ + "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 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" +} \ No newline at end of file diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 8f9b434..72a72ad 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -17,8 +17,11 @@ import { offset, } from "@phalcode/ts-igdb-client"; import { File, Metadata } from "./database.js"; -import debugPrint from "./debugprint.js"; import TaskQueue from "./taskqueue.js"; +import { singleLineStatus } from "./debugprint.js"; +import { Timer } from "./time.js"; +import { readFileSync } from "fs"; +import { dirname, resolve } from "path"; export default class MetadataSearch { constructor() { @@ -49,16 +52,23 @@ export default class MetadataSearch { "platforms.name", "game_type.type", "screenshots.image_id", - "videos.video_id" + "videos.video_id", ]; + getPlatformMapping() { + + } + 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/igdb_platform_map.json"; + this.platformMap = JSON.parse(readFileSync(mapFilePath, "utf8")); if (this.accessToken) { this.authorized = true; + this.syncAllMetadata(); return; } } @@ -68,88 +78,6 @@ export default class MetadataSearch { } } - async getMetadata(query, retrying = false) { - try { - if (!this.authorized) return; - const { data } = await this.client - .multi(...this.buildGameMultiQuery(query)) - .execute(); - return data; - } catch (error) { - if (error === "ERR_BAD_REQUEST" && !retrying) { - this.setupClient(); - return this.getMetadata(query, true); - } - console.error("Failed to retrieve metadata:", error); - } - } - - buildGameMultiQuery(query) { - let multiQuery = []; - for (let x in query) { - multiQuery.push( - request("games") - .alias(x) - .pipe( - fields(this.gameFields), - or( - and( - ...this.buildAndClauses("name", "~", query[x].name), - where("game_type.type", "!=", "Mod"), - where("game_type.type", "!=", "DLC"), - ...this.buildPlatformClause("~", query[x].platform) - ), - and( - ...this.buildAndClauses( - "alternative_names.name", - "~", - query[x].name - ), - where("game_type.type", "!=", "Mod"), - where("game_type.type", "!=", "DLC"), - ...this.buildPlatformClause("~", query[x].platform) - ), - and( - ...this.buildAndClauses( - "game_localizations.name", - "~", - query[x].name - ), - where("game_type.type", "!=", "Mod"), - where("game_type.type", "!=", "DLC"), - ...this.buildPlatformClause("~", query[x].platform) - ) - ), - sort("name", "asc"), - limit(1) - ) - ); - } - return multiQuery; - } - - buildAndClauses(field, op, string) { - let andClauses = []; - let name = [...new Set(string.split(" "))].filter((n) => n); //dedupe; - for (let x in name) { - andClauses.push(where(field, op, name[x], WhereFlags.CONTAINS)); - } - return andClauses; - } - - buildPlatformClause(op, string) { - if (string == "Others") return []; - //special garbage because SOMEONE doesn't value consistency - string = string.replace("Nintendo Wii", "Wii"); - string = string.replace("Nintendo Game Boy", "Game Boy"); - string = string.replace("Nintendo Satellaview", "Satellaview"); - string = string.replace("Sony PlayStation", "PlayStation"); - string = string.replace("Microsoft Xbox", "Xbox"); - string = string.replace("Commodore 64", "Commodore C64"); - string = string.replace("Commodore Amiga", "Amiga"); - return [where("platforms.name", op, string, WhereFlags.CONTAINS)]; - } - normalizeName(filename) { if (!filename) return; return filename @@ -161,58 +89,102 @@ export default class MetadataSearch { .trim(); } - async getGamesMetadata(games) { + async getIGDBGamesCount(retrying = false) { try { - if (!this.authorized || !games.length) return; - let gameQuery = []; - for (let x in games) { - if (!(await games[x].getDetails())) - if (!games[x].nongame) { - if (!games[x].blockmetadata) { - gameQuery.push({ - name: this.normalizeName(games[x].filename), - platform: games[x].category, - id: x, - }); - } - } - } - if (!gameQuery.length) return; - let gameMetas = await this.queue.enqueue(this.getMetadata, this, gameQuery) - debugPrint(JSON.stringify(gameMetas, null, 2)); - if (!gameMetas.length) return; - for (let x in gameMetas) { - if (gameMetas[x].result.length) { - await this.addMetadataToDb( - gameMetas[x].result[0], - games[gameQuery[x].id] - ); - } else { - games[x].blockmetadata = true; - games[x].save(); - } - } + if (!this.authorized) 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) { - console.error("Error getting metadata:", 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 queueGetGamesMetadata(games) { - try { - await this.getGamesMetadata(games); //we don't actually care as long as it finishes - let details = await Promise.all(games.map((game) => game.getDetails())); - let combined = []; - //make sure the metadata gets included with the gamedata - for (let x in games) { - combined.push({ - file: games[x].dataValues, - metadata: details[x]?.dataValues, - }); + async matchAllMetadata() { + let games = await File.findAndCountAll({ + where: { + nongame: false, + }, + limit: 1000, + }); + for (let x in games) { + let game = games[x]; + let metadata = await Metadata.searchByText(game.filename, game.category); + if (metadata) { + await game.setDetails(metadata); + await metadata.addFile(game); + } + } + } + + async syncAllMetadata(retrying = false) { + try { + const timer = new Timer(); + 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) + ) + .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; } - return combined; } catch (error) { - console.error("Error getting metadata:", error); - return [] + if (error.code === "ERR_BAD_REQUEST" && !retrying) { + this.setupClient(); + return this.syncAllMetadata(true); + } + console.error("Error syncing all metadata:", error); } } @@ -223,23 +195,6 @@ export default class MetadataSearch { md = await Metadata.build( { id: metadata.id, - title: metadata.name, - - description: metadata.summary, - rating: metadata.total_rating, - coverartid: metadata.cover?.image_id, - releasedate: metadata.first_release_date - ? new Date(metadata.first_release_date * 1000) - : null, - genre: JSON.stringify(metadata.genres?.map((genre) => genre.name)), - gamemodes: JSON.stringify( - metadata.game_modes?.map((gm) => gm.name) - ), - platforms: JSON.stringify( - metadata.platforms?.map((platform) => platform.name) - ), - screenshots: JSON.stringify(metadata.screenshots?.map((ss) => ss.image_id)), - videos: JSON.stringify(metadata.videos?.map((v) => v.video_id)) }, { returning: true, @@ -248,17 +203,25 @@ export default class MetadataSearch { } ); } - //these don't work right unless I do them after the fact. - md.developers = JSON.stringify( - metadata.involved_companies - ?.filter((ic) => ic.developer) - ?.map((ic) => ic.company.name) - ); - md.publishers = JSON.stringify( - metadata.involved_companies - ?.filter((ic) => ic.publisher) - ?.map((ic) => ic.company.name) - ); + md.title = metadata.name; + + md.description = metadata.summary; + md.rating = metadata.total_rating; + md.coverartid = metadata.cover?.image_id; + 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); let alternates = []; if (metadata.alternative_names) { alternates.push( @@ -276,12 +239,18 @@ export default class MetadataSearch { })) ); } + //this needs to remain json as we want the keys to be retained md.alternatetiles = JSON.stringify(alternates); await md.save(); - await game.setDetails(md); - await md.addFile(game); + if (game) { + await game.setDetails(md); + await md.addFile(game); + } } catch (error) { console.error("Error adding metadata:", error); } } + async sleep(delay) { + return new Promise((resolve) => setTimeout(resolve, delay)); + } } diff --git a/lib/models/metadata.js b/lib/models/metadata.js index 5fb7d93..a4d5b97 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -1,58 +1,105 @@ -import { DataTypes } from "sequelize" +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, + 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(1024), + }, + description: { + type: DataTypes.STRING(16384), + }, + rating: { + type: DataTypes.STRING, + }, + coverartid: { + type: DataTypes.STRING, + }, + 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), + }, + searchVector: { + type: DataTypes.TSVECTOR, + allowNull: true, + }, + }, + { + indexes: [ + { fields: ["title"] }, + { + name: "metadata_search_idx", + using: "gin", + fields: ["searchVector"], }, - title: { - type: DataTypes.STRING, - allowNull: false - }, - alternatetitles: { - type: DataTypes.STRING - }, - description: { - type: DataTypes.STRING(2048) - }, - rating: { - type: DataTypes.STRING - }, - coverartid: { - type: DataTypes.STRING - }, - releasedate: { - type: DataTypes.DATEONLY - }, - //anything that stores as json make the limit much higher - genre: { - type: DataTypes.STRING(2048) - }, - developers: { - type: DataTypes.STRING(2048) - }, - publishers: { - type: DataTypes.STRING(2048) - }, - gamemodes:{ - type: DataTypes.STRING(2048) - }, - platforms: { - type: DataTypes.STRING(2048) - }, - screenshots: { - type: DataTypes.STRING(2048) - }, - videos:{ - type: DataTypes.STRING(2048) - } - }, { - indexes: [ - { fields: ['title'] }, - { fields: ['description'] },//If this slows down the db may want to not index this. - ] - }) - - return Metadata -} \ No newline at end of file + ], + } + ); + + Metadata.beforeSave("addVector", async (instance) => { + const title = instance.title || ""; + const query = ` + SELECT to_tsvector('english', $1) + `; + const [results] = await sequelize.query(query, { + bind: [title], + raw: true, + }); + instance.searchVector = results[0].to_tsvector; + }); + + // Add a class method for full-text search + Metadata.searchByText = async function (searchQuery, platform, limit = 1) { + let platformClause = ""; + let limitClause = `limit ${limit}`; + if (platform) { + platformClause = `AND '${platform}' = ANY(platforms)`; + } + const query = ` + SELECT * FROM "Metadata" + WHERE "searchVector" @@ plainto_tsquery('english', :search) :platformClause + ORDER BY ts_rank("searchVector", plainto_tsquery('english', :search)) DESC :limit + `; + return await sequelize.query(query, { + model: Metadata, + replacements: { + search: searchQuery, + platformClause: platformClause, + limit: limitClause, + }, + type: sequelize.QueryTypes.SELECT, + }); + }; + + return Metadata; +} From 6c2b3c49ef4252162957400449e66689265c7e85 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 29 May 2025 10:15:39 -0600 Subject: [PATCH 29/46] add normalize, add a related name search --- lib/json/relatedkeywords/names.json | 2 +- lib/metadatasearch.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/json/relatedkeywords/names.json b/lib/json/relatedkeywords/names.json index dfe4787..4119b7a 100644 --- a/lib/json/relatedkeywords/names.json +++ b/lib/json/relatedkeywords/names.json @@ -13,5 +13,5 @@ ["bros", "brothers", "bros."], ["&", "and"], ["+", "plus"], - [".hack", "dothack"] + [".hack", "dothack", "dot hack"] ] diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 72a72ad..7a74095 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -121,7 +121,7 @@ export default class MetadataSearch { }); for (let x in games) { let game = games[x]; - let metadata = await Metadata.searchByText(game.filename, game.category); + let metadata = await Metadata.searchByText(this.normalizeName(game.filename), game.category); if (metadata) { await game.setDetails(metadata); await metadata.addFile(game); From 2973211b7d6a8f15d6f9ab34aa705e4b9e903fc5 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 29 May 2025 13:07:08 -0600 Subject: [PATCH 30/46] further tweaks --- config/locales/en.json | 5 ++- lib/dboptimize.js | 7 +++- lib/metadatasearch.js | 60 +++++++++++++++++++++++++++-------- lib/models/file.js | 5 --- lib/models/metadata.js | 24 +++++++------- lib/services/elasticsearch.js | 4 +-- server.js | 18 +++++------ views/pages/info.ejs | 3 ++ views/partials/result.ejs | 2 +- 9 files changed, 83 insertions(+), 45 deletions(-) diff --git a/config/locales/en.json b/config/locales/en.json index b58161e..e86876f 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -36,7 +36,10 @@ "filename": "Filename:", "release_group": "Release group:", "upload_date": "Upload date:", - "more_info": "More Info" + "more_info": "More Info", + "size": "Size:", + "old_experience": "Using the old search experience.", + "new_experience": "Using the new search experience." }, "about": { "title": "About", diff --git a/lib/dboptimize.js b/lib/dboptimize.js index 4b9f764..269581a 100644 --- a/lib/dboptimize.js +++ b/lib/dboptimize.js @@ -35,10 +35,15 @@ export async function optimizeDatabaseKws() { let optimizeTasks = []; let resolvedTasks = []; for (let i = 0; i < dbLength; ) { - singleLineStatus(`Optimizing Keywords: ${i} / ${dbLength}`); + singleLineStatus( + `Optimizing Keywords: ${i} / ${dbLength} ${((i / dbLength) * 100).toFixed( + 2 + )} (${proctime.elapsed()})}` + ); let result = await File.findAndCountAll({ limit: BATCH_SIZE, offset: i, + order: ["id"], }); for (let x = 0; x < result.rows.length; x++) { debugPrint(`Submitting job for: ${result.rows[x]["filename"]}`); diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 7a74095..c67b7e2 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -22,6 +22,8 @@ import { singleLineStatus } from "./debugprint.js"; import { Timer } from "./time.js"; import { readFileSync } from "fs"; import { dirname, resolve } from "path"; +import { Model } from "sequelize"; +import { Console } from "console"; export default class MetadataSearch { constructor() { @@ -55,9 +57,7 @@ export default class MetadataSearch { "videos.video_id", ]; - getPlatformMapping() { - - } + getPlatformMapping() {} async setupClient() { try { @@ -116,17 +116,47 @@ export default class MetadataSearch { let games = await File.findAndCountAll({ where: { nongame: false, + detailsId: null }, limit: 1000, + order: ["id"], }); - for (let x in games) { - let game = games[x]; - let metadata = await Metadata.searchByText(this.normalizeName(game.filename), game.category); - if (metadata) { - await game.setDetails(metadata); - await metadata.addFile(game); + let count = games.count; + let pages = Math.ceil(games.count / 1000); + let timer = new Timer(); + let found = 0; + console.log(`Matching ${count} games to metadata.`); + for (let x = 0; x < pages; x++) { + games = await File.findAndCountAll({ + where: { + nongame: false, + detailsId: null + }, + limit: 1000, + offset: x * 1000, + include: { model: Metadata, as: "details" }, + }); + for (let y = 0; y < games.rows.length; y++) { + singleLineStatus( + `Matching metadata: ${x * 1000 + y} / ${count} ${( + ((x * 1000 + y) / count) * + 100 + ).toFixed(2)}% (${timer.elapsed()}) Total Matches: ${found}` + ); + let game = games.rows[y]; + let metadata = await Metadata.searchByText( + this.normalizeName(game.filename), + game.category + ); + if (metadata.length) { + let md = await Metadata.findByPk(metadata[0].id); + await game.setDetails(md); + await md.addFile(game); + found++; + } } } + console.log(`Completed matching metadata to files in ${timer.elapsed()}`) } async syncAllMetadata(retrying = false) { @@ -179,6 +209,8 @@ export default class MetadataSearch { } retryCount = 0; } + console.log(`Finished syncing metadata in ${timer.elapsed()}`); + this.matchAllMetadata() } catch (error) { if (error.code === "ERR_BAD_REQUEST" && !retrying) { this.setupClient(); @@ -197,9 +229,7 @@ export default class MetadataSearch { id: metadata.id, }, { - returning: true, - updateOnDuplicate: ["id"], - include: File, + include: File } ); } @@ -213,7 +243,9 @@ export default class MetadataSearch { : 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.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 @@ -240,7 +272,7 @@ export default class MetadataSearch { ); } //this needs to remain json as we want the keys to be retained - md.alternatetiles = JSON.stringify(alternates); + md.alternatetitles = alternates.length ? JSON.stringify(alternates) : null; await md.save(); if (game) { await game.setDetails(md); diff --git a/lib/models/file.js b/lib/models/file.js index 754ba21..a514da2 100644 --- a/lib/models/file.js +++ b/lib/models/file.js @@ -51,11 +51,6 @@ export default function (sequelize) { group: { type: DataTypes.TEXT }, - blockmetadata: { - type: DataTypes.BOOLEAN, - defaultValue: false, - allowNull: false - }, nongame: { type: DataTypes.BOOLEAN, defaultValue: false, diff --git a/lib/models/metadata.js b/lib/models/metadata.js index a4d5b97..2dc5416 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -14,7 +14,7 @@ export default function (sequelize) { allowNull: false, }, alternatetitles: { - type: DataTypes.STRING(1024), + type: DataTypes.STRING(4096), }, description: { type: DataTypes.STRING(16384), @@ -68,11 +68,15 @@ export default function (sequelize) { Metadata.beforeSave("addVector", async (instance) => { const title = instance.title || ""; + const alternateTitles = + JSON.parse(instance.alternatetitles || "[]") + .map((title) => title.name) + .join(", ") || ""; const query = ` - SELECT to_tsvector('english', $1) + SELECT to_tsvector('english', $1 || ', ' || $2) `; const [results] = await sequelize.query(query, { - bind: [title], + bind: [title, alternateTitles], raw: true, }); instance.searchVector = results[0].to_tsvector; @@ -81,22 +85,18 @@ export default function (sequelize) { // Add a class method for full-text search Metadata.searchByText = async function (searchQuery, platform, limit = 1) { let platformClause = ""; - let limitClause = `limit ${limit}`; + let limitClause = `LIMIT ${limit}`; if (platform) { platformClause = `AND '${platform}' = ANY(platforms)`; } const query = ` - SELECT * FROM "Metadata" - WHERE "searchVector" @@ plainto_tsquery('english', :search) :platformClause - ORDER BY ts_rank("searchVector", plainto_tsquery('english', :search)) DESC :limit + SELECT id FROM "Metadata" + WHERE "searchVector" @@ plainto_tsquery('english', $1) ${platformClause} + ORDER BY ts_rank("searchVector", plainto_tsquery('english', $1 )) ${limitClause} `; return await sequelize.query(query, { model: Metadata, - replacements: { - search: searchQuery, - platformClause: platformClause, - limit: limitClause, - }, + bind: [searchQuery], type: sequelize.QueryTypes.SELECT, }); }; diff --git a/lib/services/elasticsearch.js b/lib/services/elasticsearch.js index f63858f..cbbaf5b 100644 --- a/lib/services/elasticsearch.js +++ b/lib/services/elasticsearch.js @@ -168,7 +168,8 @@ export async function search(query, options) { // Fetch full records from PostgreSQL for the search results 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 @@ -192,7 +193,6 @@ export async function search(query, options) { const elapsed = timer.elapsedSeconds(); return { items: results, - db: fullRecords, count: response.hits.total.value || 0, elapsed, }; diff --git a/server.js b/server.js index c141caf..56b86bb 100644 --- a/server.js +++ b/server.js @@ -1,4 +1,5 @@ import getAllFiles from "./lib/dircrawl.js"; +import { optimizeDatabaseKws } from "./dboptimize.js"; import FileHandler from "./lib/filehandler.js"; import Searcher from "./lib/search.js"; import cron from "node-cron"; @@ -16,11 +17,10 @@ import { isNonGameContent, } from "./lib/emulatorConfig.js"; import fetch from "node-fetch"; -import { initDB, File, QueryCount } from "./lib/database.js"; +import { initDB, File, QueryCount, Metadata } from "./lib/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 MetadataSearch from "./lib/metadatasearch.js"; import Flag from "./lib/flag.js"; import ConsoleIcons from "./lib/consoleicons.js"; @@ -81,6 +81,10 @@ async function getFilesJob() { } crawlTime = Date.now(); console.log(`Finished updating file list. ${fileCount} found.`); + if(Metadata.count() > metadataSearch.getIGDBGamesCount()){ + await metadataSearch.syncAllMetadata(); + } + optimizeDatabaseKws(); } function buildOptions(page, options) { @@ -205,16 +209,12 @@ app.get("/search", async function (req, res) { delete settings.combineWith; } let loadOldResults = - req.query.old === "true" || !metadataSearch.authorized ? true : false; + req.query.old === "true" || !Metadata.count() ? true : false; settings.pageSize = loadOldResults ? 100 : 10; settings.page = pageNum - 1; settings.sort = req.query.o || ""; let results = await search.findAllMatches(query, settings); debugPrint(results); - let metas = []; - if (!loadOldResults) { - metas = await metadataSearch.queueGetGamesMetadata(results.db); - } if (results.count && pageNum == 1) { queryCount += 1; await QueryCount.update({ count: queryCount }, { where: { id: 1 } }); @@ -222,7 +222,7 @@ app.get("/search", async function (req, res) { } let options = { query: query, - results: metas?.length ? metas : results.items, + results: results.items, count: results.count, elapsed: results.elapsed, pageNum: pageNum, @@ -266,7 +266,7 @@ app.get("/lucky", async function (req, res) { app.get("/settings", function (req, res) { let options = { defaultSettings: defaultSettings }; let page = "settings"; - options.oldSettingsAvailable = metadataSearch.authorized; + options.oldSettingsAvailable = Metadata.count() ? true : false; options = buildOptions(page, options); res.render(indexPage, options); }); diff --git a/views/pages/info.ejs b/views/pages/info.ejs index 69d13a6..b7b5cd8 100644 --- a/views/pages/info.ejs +++ b/views/pages/info.ejs @@ -78,6 +78,9 @@

                <%= __('search.filename') %> <%= file.filename %>

                +
                +

                <%= __('search.size') %> <%= file.size %>

                +

                <%= __('search.upload_date') %> <%= file.date %>

                diff --git a/views/partials/result.ejs b/views/partials/result.ejs index 45db1da..30de365 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -22,7 +22,7 @@

                <%= __('search.no_metadata') %>

                <% } %> <% if(metadata.title) {%> -

                <%= __('search.filename') %> <%= file.filename %> | <%= __('search.upload_date')%> <%= file.date %>

                +

                <%= __('search.filename') %> <%= file.filename %> | <%= __('search.size')%> <%= file.size %> | <%= __('search.upload_date')%> <%= file.date %>

                <% } %>

                <%= __('search.release_group') %> <%= file.group %>

                From 869d9c72eb395a95fa9ff42cfdb311f7ee93112b Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 29 May 2025 16:08:01 -0600 Subject: [PATCH 31/46] Improvements to regional handling additional cleanup and stuff I lost the plot a while ago --- lib/database.js | 88 +++++++++++------- lib/dboptimize.js | 2 +- lib/dircrawl.js | 2 - .../igdb_platform.json} | 1 + lib/metadatasearch.js | 89 ++++++++++++------- lib/models/metadata.js | 35 ++++++-- lib/services/elasticsearch.js | 10 +-- server.js | 2 +- views/pages/info.ejs | 14 +-- views/partials/result.ejs | 6 +- 10 files changed, 164 insertions(+), 85 deletions(-) rename lib/json/{igdb_platform_map.json => maps/igdb_platform.json} (99%) diff --git a/lib/database.js b/lib/database.js index 1c8552d..035d69a 100644 --- a/lib/database.js +++ b/lib/database.js @@ -1,42 +1,63 @@ -import { Sequelize } from 'sequelize'; -import 'dotenv/config'; +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' +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 -}); +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"}) +export const Metadata = defineMetadata(sequelize); +Metadata.hasMany(File); +File.belongsTo(Metadata, { as: "details" }); + +async function enableTrigram() { + const query = `SELECT * from PG_extension where extname = 'pg_trgm'`; + const [result] = await sequelize.query(query, { + type: sequelize.QueryTypes.SELECT, + }); + if (!result) { + const enableTrigramQuery = `CREATE EXTENSION pg_trgm`; + await sequelize.query(enableTrigramQuery); + } +} 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 - }); + 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.'); + console.log("Database did not exist, created."); } catch (err) { // Ignore error if database already exists - if (!err.message.includes('already exists')) { + if (!err.message.includes("already exists")) { throw err; } } finally { @@ -45,36 +66,39 @@ export async function initDB() { // Now connect to the actual database await sequelize.authenticate(); - console.log('DB connected.'); + 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.includes("Files") || !tables.includes("QueryCounts")) { // If tables don't exist, create them - console.log('DB doesn\'t exist, creating initial database schema...'); + console.log("DB doesn't exist, creating initial database schema..."); await sequelize.sync(); - console.log('Database schema created.'); + 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...'); + console.log("Checking for DB migrations..."); await sequelize.sync({ alter: true }); - console.log('DB migrations completed.'); + await enableTrigram(); + console.log("DB migrations completed."); } // Only force sync if explicitly requested - if (process.env.FORCE_FILE_REBUILD === '1') { + if (process.env.FORCE_FILE_REBUILD === "1") { await sequelize.sync({ force: true }); - console.log('DB forcefully synchronized.'); + await enableTrigram(); + console.log("DB forcefully synchronized."); } } catch (error) { - console.error('Unable to connect to the DB:', error); + console.error("Unable to connect to the DB:", error); process.exit(1); } } -export default sequelize; \ No newline at end of file +export default sequelize; diff --git a/lib/dboptimize.js b/lib/dboptimize.js index 269581a..a603b8f 100644 --- a/lib/dboptimize.js +++ b/lib/dboptimize.js @@ -43,7 +43,7 @@ export async function optimizeDatabaseKws() { let result = await File.findAndCountAll({ limit: BATCH_SIZE, offset: i, - order: ["id"], + order: ["id", "filename"], }); for (let x = 0; x < result.rows.length; x++) { debugPrint(`Submitting job for: ${result.rows[x]["filename"]}`); diff --git a/lib/dircrawl.js b/lib/dircrawl.js index caca3d6..4939cc8 100644 --- a/lib/dircrawl.js +++ b/lib/dircrawl.js @@ -4,7 +4,6 @@ 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"; let piscina = new Piscina({ @@ -146,7 +145,6 @@ export default async function getAllFiles(catList) { console.log(`\nFinished crawling Myrient in ${proctime.elapsed()}.`); await piscina.close(); - await optimizeDatabaseKws(); return fileCount; } diff --git a/lib/json/igdb_platform_map.json b/lib/json/maps/igdb_platform.json similarity index 99% rename from lib/json/igdb_platform_map.json rename to lib/json/maps/igdb_platform.json index 1df4aa7..74d3330 100644 --- a/lib/json/igdb_platform_map.json +++ b/lib/json/maps/igdb_platform.json @@ -25,6 +25,7 @@ "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", diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index c67b7e2..461f5a8 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -21,9 +21,6 @@ import TaskQueue from "./taskqueue.js"; import { singleLineStatus } from "./debugprint.js"; import { Timer } from "./time.js"; import { readFileSync } from "fs"; -import { dirname, resolve } from "path"; -import { Model } from "sequelize"; -import { Console } from "console"; export default class MetadataSearch { constructor() { @@ -51,6 +48,7 @@ export default class MetadataSearch { "game_localizations.name", "game_localizations.region", "game_localizations.region.name", + "game_localizations.cover.image_id", "platforms.name", "game_type.type", "screenshots.image_id", @@ -64,7 +62,7 @@ export default class MetadataSearch { 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/igdb_platform_map.json"; + const mapFilePath = "./lib/json/maps/igdb_platform.json"; this.platformMap = JSON.parse(readFileSync(mapFilePath, "utf8")); if (this.accessToken) { this.authorized = true; @@ -116,10 +114,10 @@ export default class MetadataSearch { let games = await File.findAndCountAll({ where: { nongame: false, - detailsId: null + //detailsId: null, }, limit: 1000, - order: ["id"], + order: ["id", "filename"], }); let count = games.count; let pages = Math.ceil(games.count / 1000); @@ -130,10 +128,11 @@ export default class MetadataSearch { games = await File.findAndCountAll({ where: { nongame: false, - detailsId: null + //detailsId: null, }, limit: 1000, offset: x * 1000, + order: ["id", "filename"], include: { model: Metadata, as: "details" }, }); for (let y = 0; y < games.rows.length; y++) { @@ -152,11 +151,27 @@ export default class MetadataSearch { let md = await Metadata.findByPk(metadata[0].id); await game.setDetails(md); await md.addFile(game); + await game.save(); + await md.save(); found++; + } else { + //this is much slower and should only be used if the faster full text search can't find it. + let metadata = Metadata.fuzzySearchByText( + this.normalizeName(game.filename), + 0.6, + game.category + ); + if (metadata) { + await game.setDetails(md); + await md.addFile(game); + await game.save(); + await md.save(); + found++; + } } } } - console.log(`Completed matching metadata to files in ${timer.elapsed()}`) + console.log(`\nFinished matching metadata to files in ${timer.elapsed()}`); } async syncAllMetadata(retrying = false) { @@ -187,7 +202,8 @@ export default class MetadataSearch { .pipe( limit(pageSize), offset(x * pageSize), - fields(this.gameFields) + fields(this.gameFields), + sort("id") ) .execute(); for (let y in data) { @@ -209,8 +225,8 @@ export default class MetadataSearch { } retryCount = 0; } - console.log(`Finished syncing metadata in ${timer.elapsed()}`); - this.matchAllMetadata() + console.log(`\nFinished syncing metadata in ${timer.elapsed()}`); + this.matchAllMetadata(); } catch (error) { if (error.code === "ERR_BAD_REQUEST" && !retrying) { this.setupClient(); @@ -220,7 +236,7 @@ export default class MetadataSearch { } } - async addMetadataToDb(metadata, game) { + async addMetadataToDb(metadata) { try { let md = await Metadata.findByPk(metadata.id); if (!md) { @@ -229,15 +245,25 @@ export default class MetadataSearch { id: metadata.id, }, { - include: File + 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 = metadata.cover?.image_id; + md.coverartid = JSON.stringify(coverArt); md.releasedate = metadata.first_release_date ? new Date(metadata.first_release_date * 1000) : null; @@ -254,30 +280,29 @@ export default class MetadataSearch { md.publishers = metadata.involved_companies ?.filter((ic) => ic.publisher) ?.map((ic) => ic.company.name); - let alternates = []; + //I hate this too + let alternates = new Object(); if (metadata.alternative_names) { - alternates.push( - metadata.alternative_names.map((an) => ({ - type: an.comment, - name: an.name, - })) - ); + 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) { - alternates.push( - metadata.game_localizations.map((gn) => ({ - type: gn.region.name, - name: gn.name, - })) - ); + 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.length ? JSON.stringify(alternates) : null; + md.alternatetitles = alternates.length + ? JSON.stringify(alternates) + : null; await md.save(); - if (game) { - await game.setDetails(md); - await md.addFile(game); - } } catch (error) { console.error("Error adding metadata:", error); } diff --git a/lib/models/metadata.js b/lib/models/metadata.js index 2dc5416..783c100 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -23,7 +23,7 @@ export default function (sequelize) { type: DataTypes.STRING, }, coverartid: { - type: DataTypes.STRING, + type: DataTypes.STRING(2048), }, releasedate: { type: DataTypes.DATEONLY, @@ -70,13 +70,12 @@ export default function (sequelize) { const title = instance.title || ""; const alternateTitles = JSON.parse(instance.alternatetitles || "[]") - .map((title) => title.name) - .join(", ") || ""; + const titles = Object.values(alternateTitles).join(', ') const query = ` SELECT to_tsvector('english', $1 || ', ' || $2) `; const [results] = await sequelize.query(query, { - bind: [title, alternateTitles], + bind: [title, titles], raw: true, }); instance.searchVector = results[0].to_tsvector; @@ -92,7 +91,7 @@ export default function (sequelize) { const query = ` SELECT id FROM "Metadata" WHERE "searchVector" @@ plainto_tsquery('english', $1) ${platformClause} - ORDER BY ts_rank("searchVector", plainto_tsquery('english', $1 )) ${limitClause} + ORDER BY length(title) ${limitClause} `; return await sequelize.query(query, { model: Metadata, @@ -101,5 +100,31 @@ export default function (sequelize) { }); }; + Metadata.fuzzySearchByText = async function ( + searchQuery, + fuzziness, + platform, + limit = 1 + ) { + fuzziness = fuzziness || 0.6; + let platformClause = ""; + let limitClause = `LIMIT ${limit}`; + if (platform) { + platformClause = `AND '${platform}' = ANY(platforms)`; + } + const query = ` + SELECT id FROM "Metadata" + WHERE SIMILARITY(title, $1) > $2 OR WHERE SIMILARITY(alternatetitles, $1) > $2 + ${platformClause} + ORDER BY length(title) ${limitClause} + `; + + return await sequelize.query(query, { + model: Metadata, + bind: [searchQuery, fuzziness], + type: sequelize.QueryTypes.SELECT, + }); + }; + return Metadata; } diff --git a/lib/services/elasticsearch.js b/lib/services/elasticsearch.js index cbbaf5b..fe58409 100644 --- a/lib/services/elasticsearch.js +++ b/lib/services/elasticsearch.js @@ -1,6 +1,6 @@ import { Client } from "@elastic/elasticsearch"; import debugPrint from "../debugprint.js"; -import { File } from "../models/index.js"; +import { File, Metadata } from "../models/index.js"; import { Timer } from "../time.js"; const client = new Client({ @@ -181,15 +181,15 @@ export async function search(query, options) { // Build results with full PostgreSQL records let results = response.hits.hits.map((hit) => ({ file: { - ...recordMap[hit._id]?.dataValues, + ...recordMap[hit._id]?.dataValues + }, + metadata: { + ...recordMap[hit._id]?.details?.dataValues }, score: hit._score, highlights: hit.highlight, })); - //Filter out anything that couldn't be found in postgres - results = results.filter((result) => result.file.filename); - const elapsed = timer.elapsedSeconds(); return { items: results, diff --git a/server.js b/server.js index 56b86bb..e9e90a2 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,5 @@ import getAllFiles from "./lib/dircrawl.js"; -import { optimizeDatabaseKws } from "./dboptimize.js"; +import { optimizeDatabaseKws } from "./lib/dboptimize.js"; import FileHandler from "./lib/filehandler.js"; import Searcher from "./lib/search.js"; import cron from "node-cron"; diff --git a/views/pages/info.ejs b/views/pages/info.ejs index b7b5cd8..a26f89f 100644 --- a/views/pages/info.ejs +++ b/views/pages/info.ejs @@ -1,7 +1,11 @@ <% const metadata = romFile.metadata || new Object() const file = romFile.file || new Object() - const coverUrl = metadata.coverartid ? `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_cover_big/${metadata.coverartid}.webp` : "/public/images/coverart/nocoverart.png" + const titles = JSON.parse(metadata.alternatetitles) + + const coverarts = 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 = JSON.parse(metadata.screenshots).map((im) => `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_720p/${im}.webp`) @@ -47,12 +51,12 @@ <% } %> <% if(metadata.developers) {%>

                -

                <%= __('search.developed') %> <%= JSON.parse(metadata.developers).join(", ") %>

                +

                <%= __('search.developed') %> <%= metadata.developers.join(", ") %>

                <% } %> <% if(metadata.publishers) {%>
                -

                <%= __('search.published') %> <%= JSON.parse(metadata.publishers).join(", ") %>

                +

                <%= __('search.published') %> <%= metadata.publishers.join(", ") %>

                <% } %> <% if(metadata.releasedate) {%> @@ -67,12 +71,12 @@ <% } %> <% if(metadata.genre) {%>
                -

                <%= __('search.genre') %> <%= JSON.parse(metadata.genre).join(", ") %>

                +

                <%= __('search.genre') %> <%= metadata.genre.join(", ") %>

                <% } %> <% if(metadata.gamemodes) {%>
                -

                <%= __('search.modes') %> <%= JSON.parse(metadata.gamemodes).join(", ") %>

                +

                <%= __('search.modes') %> <%= metadata.gamemodes.join(", ") %>

                <% } %>
                diff --git a/views/partials/result.ejs b/views/partials/result.ejs index 30de365..8cab2b5 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -1,7 +1,9 @@ <% const metadata = result.metadata || new Object() const file = result.file || new Object() - const coverUrl = metadata.coverartid ? `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_cover_big/${metadata.coverartid}.webp` : "/public/images/coverart/nocoverart.png" + const coverarts = 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" %>
                @@ -13,7 +15,7 @@ <%= __('search.region') %> <%= file.region %> <%- flags.createFlag(file.region) %> <%= __('search.platform') %> <%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %> <% if(metadata.genre){ %> - <%= __('search.genre') %> <%= JSON.parse(metadata.genre).join(' / ') %> + <%= __('search.genre') %> <%= metadata.genre.join(' / ') %> <% } %>

                <% if(metadata.title) {%> From d7e1822843d9d7d8f331edb01113bd0f78c0fb18 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 29 May 2025 16:24:12 -0600 Subject: [PATCH 32/46] implemetnation of names matching locale --- lib/json/maps/name_localization.json | 18 ++++++++++++++++++ server.js | 8 ++++++-- views/pages/info.ejs | 10 ++++++++-- views/partials/result.ejs | 10 +++++++++- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 lib/json/maps/name_localization.json diff --git a/lib/json/maps/name_localization.json b/lib/json/maps/name_localization.json new file mode 100644 index 0000000..06f7ac2 --- /dev/null +++ b/lib/json/maps/name_localization.json @@ -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": "Romaji", + "zh": "Chinese" +} \ No newline at end of file diff --git a/server.js b/server.js index e9e90a2..f948776 100644 --- a/server.js +++ b/server.js @@ -28,9 +28,11 @@ import ConsoleIcons from "./lib/consoleicons.js"; let categoryListPath = "./lib/categories.json"; let nonGameTermsPath = "./lib/nonGameTerms.json"; let emulatorsPath = "./lib/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; @@ -231,7 +233,8 @@ app.get("/search", async function (req, res) { urlPrefix: urlPrefix, settings: settings, flags: flags, - consoleIcons: consoleIcons + consoleIcons: consoleIcons, + localeNames: localeNames }; let page = loadOldResults ? "resultsold" : "results"; options = buildOptions(page, options); @@ -336,7 +339,8 @@ app.get("/info/:id", async function (req, res) { let options = { romFile: romInfo[0], flags: flags, - consoleIcons: consoleIcons + consoleIcons: consoleIcons, + localeNames: localeNames }; let page = "info"; options = buildOptions(page, options); diff --git a/views/pages/info.ejs b/views/pages/info.ejs index a26f89f..9fc3e13 100644 --- a/views/pages/info.ejs +++ b/views/pages/info.ejs @@ -2,7 +2,13 @@ const metadata = romFile.metadata || new Object() const file = romFile.file || new Object() const titles = JSON.parse(metadata.alternatetitles) - + let title = metadata.title + for(let x in titles){ + //display in language specific name if available + if(titles[x].includes(localeNames[lang])){ + title = titles[x] + } + } const coverarts = 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" @@ -20,7 +26,7 @@
                -

                <%= metadata.title %>

                +

                <%= title %>

                <%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %>

                diff --git a/views/partials/result.ejs b/views/partials/result.ejs index 8cab2b5..7e34cb2 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -1,6 +1,14 @@ <% const metadata = result.metadata || new Object() const file = result.file || new Object() + const titles = JSON.parse(metadata.alternatetitles) + let title = metadata.title + for(let x in titles){ + //display in language specific name if available + if(titles[x].includes(localeNames[lang])){ + title = titles[x] + } + } const coverarts = 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" @@ -10,7 +18,7 @@
                -

                <%= metadata.title || file.filename %>

                +

                <%= title || file.filename %>

                <%= __('search.released') %> <%= metadata.releasedate || file.date %> <%= __('search.region') %> <%= file.region %> <%- flags.createFlag(file.region) %> <%= __('search.platform') %> <%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %> From 85b57e6c39367450438ceb4b32d212cf3090d3e7 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 29 May 2025 16:53:31 -0600 Subject: [PATCH 33/46] make fuzzy metadata search happen after all other database operations have settled. --- lib/metadatasearch.js | 11 ++++++----- lib/models/metadata.js | 2 +- server.js | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 461f5a8..eeda3a9 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -66,7 +66,7 @@ export default class MetadataSearch { this.platformMap = JSON.parse(readFileSync(mapFilePath, "utf8")); if (this.accessToken) { this.authorized = true; - this.syncAllMetadata(); + this.matchAllMetadata(); return; } } @@ -110,7 +110,7 @@ export default class MetadataSearch { } } - async matchAllMetadata() { + async matchAllMetadata(fuzzy = false) { let games = await File.findAndCountAll({ where: { nongame: false, @@ -154,14 +154,15 @@ export default class MetadataSearch { await game.save(); await md.save(); found++; - } else { + } else if (fuzzy) { //this is much slower and should only be used if the faster full text search can't find it. - let metadata = Metadata.fuzzySearchByText( + let metadata = await Metadata.fuzzySearchByText( this.normalizeName(game.filename), 0.6, game.category ); - if (metadata) { + if (metadata.length) { + let md = await Metadata.findByPk(metadata[0].id); await game.setDetails(md); await md.addFile(game); await game.save(); diff --git a/lib/models/metadata.js b/lib/models/metadata.js index 783c100..d5fd278 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -114,7 +114,7 @@ export default function (sequelize) { } const query = ` SELECT id FROM "Metadata" - WHERE SIMILARITY(title, $1) > $2 OR WHERE SIMILARITY(alternatetitles, $1) > $2 + WHERE SIMILARITY(title, $1) > $2 OR SIMILARITY(alternatetitles, $1) > $2 ${platformClause} ORDER BY length(title) ${limitClause} `; diff --git a/server.js b/server.js index f948776..b902ae1 100644 --- a/server.js +++ b/server.js @@ -87,6 +87,7 @@ async function getFilesJob() { await metadataSearch.syncAllMetadata(); } optimizeDatabaseKws(); + metadataSearch.matchAllMetadata(true) } function buildOptions(page, options) { From 0540152826253d7b13d17445a5cb6c9c5b40dbbe Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 29 May 2025 17:18:01 -0600 Subject: [PATCH 34/46] Update translations --- config/locales/ar.json | 5 ++++- config/locales/bn.json | 5 ++++- config/locales/de.json | 5 ++++- config/locales/en.json | 2 +- config/locales/es.json | 5 ++++- config/locales/fr.json | 12 ++++++++++-- config/locales/hi.json | 6 +++++- config/locales/it.json | 5 ++++- config/locales/ja.json | 12 ++++++++++-- config/locales/ko.json | 15 ++++++++++++--- config/locales/pl.json | 8 ++++++-- config/locales/pt.json | 12 ++++++++++-- config/locales/romaji.json | 13 +++++++++++-- config/locales/ru.json | 5 ++++- config/locales/tr.json | 5 ++++- config/locales/zh.json | 12 ++++++++++-- server.js | 1 + views/pages/results.ejs | 4 ++++ views/pages/resultsold.ejs | 4 ++++ 19 files changed, 112 insertions(+), 24 deletions(-) diff --git a/config/locales/ar.json b/config/locales/ar.json index 44b8085..45494dd 100644 --- a/config/locales/ar.json +++ b/config/locales/ar.json @@ -36,7 +36,10 @@ "filename": "اسم الملف:", "release_group": "مجموعة الإصدار:", "upload_date": "تاريخ الرفع:", - "more_info": "المزيد من المعلومات" + "more_info": "المزيد من المعلومات", + "size": "الحجم:", + "old_experience": "استخدام تجربة البحث القديمة.", + "new_experience": "استخدام تجربة البحث الجديدة." }, "about": { "title": "حول الموقع", diff --git a/config/locales/bn.json b/config/locales/bn.json index fa98f31..9ed5628 100644 --- a/config/locales/bn.json +++ b/config/locales/bn.json @@ -36,7 +36,10 @@ "filename": "ফাইলের নাম:", "release_group": "রিলিজ গ্রুপ:", "upload_date": "আপলোডের তারিখ:", - "more_info": "আরও তথ্য" + "more_info": "আরও তথ্য", + "size": "সাইজ:", + "old_experience": "পুরানো অনুসন্ধান অভিজ্ঞতা ব্যবহার করা হচ্ছে।", + "new_experience": "নতুন অনুসন্ধান অভিজ্ঞতা ব্যবহার করা হচ্ছে।" }, "about": { "title": "সম্পর্কে", diff --git a/config/locales/de.json b/config/locales/de.json index 838a966..cd916a6 100644 --- a/config/locales/de.json +++ b/config/locales/de.json @@ -36,7 +36,10 @@ "filename": "Dateiname:", "release_group": "Release-Gruppe:", "upload_date": "Upload-Datum:", - "more_info": "Mehr Info" + "more_info": "Mehr Info", + "size": "Größe:", + "old_experience": "Alte Sucherfahrung wird verwendet.", + "new_experience": "Neue Sucherfahrung wird verwendet." }, "about": { "title": "Über uns", diff --git a/config/locales/en.json b/config/locales/en.json index e86876f..0941fc5 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -94,7 +94,7 @@ }, "use_old_results": { "label": "Old Search", - "tooltip": "Redirects your search to the old table formatted results. These will load faster as they will ignore pulling metadata." + "tooltip": "Controls whether your searches direct to the new or old results experience." } }, "save": "Save Settings" diff --git a/config/locales/es.json b/config/locales/es.json index 268476f..2e6a5ed 100644 --- a/config/locales/es.json +++ b/config/locales/es.json @@ -36,7 +36,10 @@ "filename": "Nombre del archivo:", "release_group": "Grupo de lanzamiento:", "upload_date": "Fecha de subida:", - "more_info": "Más información" + "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", diff --git a/config/locales/fr.json b/config/locales/fr.json index 155fe25..e8ba29c 100644 --- a/config/locales/fr.json +++ b/config/locales/fr.json @@ -36,7 +36,10 @@ "filename": "Nom du fichier :", "release_group": "Groupe de release :", "upload_date": "Date de téléchargement :", - "more_info": "Plus d'infos" + "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", @@ -88,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" @@ -114,7 +121,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.", diff --git a/config/locales/hi.json b/config/locales/hi.json index 333fbae..780fe0e 100644 --- a/config/locales/hi.json +++ b/config/locales/hi.json @@ -36,7 +36,10 @@ "filename": "फ़ाइल का नाम:", "release_group": "रिलीज़ ग्रुप:", "upload_date": "अपलोड की तारीख:", - "more_info": "अधिक जानकारी" + "more_info": "अधिक जानकारी", + "size": "आकार:", + "old_experience": "पुरानी खोज अनुभव का उपयोग किया जा रहा है।", + "new_experience": "नई खोज अनुभव का उपयोग किया जा रहा है।" }, "about": { "title": "परिचय", @@ -126,6 +129,7 @@ "more_info": "इस सेवा के बारे में अधिक जानकारी के लिए, कृपया परिचय पृष्ठ पर जाएं।" }, "recommended": "अनुशंसित एमुलेटर्स", + "download": "डाउनलोड", "play": "खेलें", "not_available": "----", "not_available_tooltip": "इस शीर्षक के लिए वेब एमुलेशन उपलब्ध नहीं है क्योंकि यह या तो एक गेम नहीं है या प्लेटफ़ॉर्म समर्थित नहीं है।", diff --git a/config/locales/it.json b/config/locales/it.json index 98dfc78..21d4df7 100644 --- a/config/locales/it.json +++ b/config/locales/it.json @@ -36,7 +36,10 @@ "filename": "Nome file:", "release_group": "Gruppo di release:", "upload_date": "Data di caricamento:", - "more_info": "Più informazioni" + "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", diff --git a/config/locales/ja.json b/config/locales/ja.json index db0750c..ff6423c 100644 --- a/config/locales/ja.json +++ b/config/locales/ja.json @@ -36,7 +36,10 @@ "filename": "ファイル名:", "release_group": "リリースグループ:", "upload_date": "アップロード日:", - "more_info": "詳細情報" + "more_info": "詳細情報", + "size": "サイズ:", + "old_experience": "旧検索エクスペリエンスを使用中。", + "new_experience": "新検索エクスペリエンスを使用中。" }, "about": { "title": "サイトについて", @@ -88,6 +91,10 @@ "hide_non_game": { "label": "非ゲームコンテンツを非表示", "tooltip": "ROMハック、パッチ、アートワーク、その他のゲーム以外のコンテンツを検索結果から除外します。" + }, + "use_old_results": { + "label": "旧検索", + "tooltip": "検索結果を新しい表示形式と旧表示形式のどちらで表示するかを切り替えます。" } }, "save": "設定を保存" @@ -114,7 +121,8 @@ "non_game": "警告:このファイルはゲームROMではない可能性があり、Webエミュレーターで正しく動作しない場合があります。", "see_about": "詳細については{{link}}ページをご覧ください。", "no_data": "エミュレーターデータがありません。", - "https": "安全でないHTTP接続:一部のエミュレーターは正常に動作するためにHTTPSが必要です。このサイトは正しく設定されていません。" + "https": "安全でないHTTP接続:一部のエミュレーターは正常に動作するためにHTTPSが必要です。このサイトは正しく設定されていません。", + "not_available_tooltip": "このタイトルはゲームではないか、プラットフォームが非対応のため、Webエミュレーションを利用できません。" }, "console": { "about": "これはMyrientの公開アーカイブから直接ゲームを実行するオンラインエミュレータです。", diff --git a/config/locales/ko.json b/config/locales/ko.json index 498884e..a69d442 100644 --- a/config/locales/ko.json +++ b/config/locales/ko.json @@ -36,7 +36,10 @@ "filename": "파일명:", "release_group": "릴리즈 그룹:", "upload_date": "업로드 날짜:", - "more_info": "자세히 보기" + "more_info": "자세히 보기", + "size": "크기:", + "old_experience": "이전 검색 환경을 사용 중입니다.", + "new_experience": "새로운 검색 환경을 사용 중입니다." }, "about": { "title": "소개", @@ -88,6 +91,10 @@ "hide_non_game": { "label": "비게임 콘텐츠 숨기기", "tooltip": "ROM 해킹, 패치, 아트워크 등 게임이 아닌 콘텐츠를 검색 결과에서 제외합니다." + }, + "use_old_results": { + "label": "이전 검색", + "tooltip": "검색 결과를 새로운 방식과 이전 방식 중 어느 것으로 표시할지 선택합니다." } }, "save": "설정 저장" @@ -114,7 +121,8 @@ "non_game": "경고: 이 파일은 게임 ROM이 아닐 수 있으며 웹 에뮬레이터에서 제대로 작동하지 않을 수 있습니다.", "see_about": "자세한 내용은 {{link}} 페이지를 참조하세요.", "no_data": "사용 가능한 에뮬레이터 데이터가 없습니다.", - "https": "안전하지 않은 HTTP: 일부 에뮬레이터는 제대로 작동하려면 HTTPS가 필요합니다. 이 사이트는 올바르게 설정되지 않았습니다." + "https": "안전하지 않은 HTTP: 일부 에뮬레이터는 제대로 작동하려면 HTTPS가 필요합니다. 이 사이트는 올바르게 설정되지 않았습니다.", + "not_available_tooltip": "이 타이틀은 게임이 아니거나 지원되지 않는 플랫폼이기 때문에 웹 에뮬레이션을 사용할 수 없습니다." }, "console": { "about": "이 온라인 에뮬레이터는 Myrient의 공개 아카이브에서 직접 게임 롬을 실행합니다.", @@ -124,7 +132,8 @@ "recommended": "추천 에뮬레이터", "play": "플레이", "not_available": "----", - "disclaimer": "이 에뮬레이터는 {{link}}에서 직접 게임을 불러옵니다. {{about}} 페이지에서 더 자세히 알아보세요." + "disclaimer": "이 에뮬레이터는 {{link}}에서 직접 게임을 불러옵니다. {{about}} 페이지에서 더 자세히 알아보세요.", + "download": "다운로드" }, "results": { "table": { diff --git a/config/locales/pl.json b/config/locales/pl.json index 1284ad7..d7a2e84 100644 --- a/config/locales/pl.json +++ b/config/locales/pl.json @@ -36,7 +36,10 @@ "filename": "Nazwa pliku:", "release_group": "Grupa wydania:", "upload_date": "Data przesłania:", - "more_info": "Więcej informacji" + "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", @@ -129,7 +132,8 @@ "play": "Graj", "not_available": "----", "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}}." + "disclaimer": "Ten emulator ładuje gry bezpośrednio z {{link}}. Dowiedz się więcej na stronie {{about}}.", + "download": "Pobierz" }, "results": { "table": { diff --git a/config/locales/pt.json b/config/locales/pt.json index d01084d..e8a7a4b 100644 --- a/config/locales/pt.json +++ b/config/locales/pt.json @@ -36,7 +36,10 @@ "filename": "Nome do arquivo:", "release_group": "Grupo de lançamento:", "upload_date": "Data de upload:", - "more_info": "Mais informações" + "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", @@ -88,6 +91,10 @@ "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" @@ -114,7 +121,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.", diff --git a/config/locales/romaji.json b/config/locales/romaji.json index 5b31368..e300ea8 100644 --- a/config/locales/romaji.json +++ b/config/locales/romaji.json @@ -36,7 +36,10 @@ "filename": "Fairu mei:", "release_group": "Rirīsu gurūpu:", "upload_date": "Apurōdo-bi:", - "more_info": "Kuwashii jōhō" + "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", @@ -88,6 +91,10 @@ "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" @@ -124,7 +131,9 @@ "recommended": "Suishou Emyurēta", "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": { diff --git a/config/locales/ru.json b/config/locales/ru.json index 330aac6..4812af6 100644 --- a/config/locales/ru.json +++ b/config/locales/ru.json @@ -36,7 +36,10 @@ "filename": "Имя файла:", "release_group": "Релиз-группа:", "upload_date": "Дата загрузки:", - "more_info": "Подробнее" + "more_info": "Подробнее", + "size": "Размер:", + "old_experience": "Используется старый интерфейс поиска.", + "new_experience": "Используется новый интерфейс поиска." }, "about": { "title": "О сайте", diff --git a/config/locales/tr.json b/config/locales/tr.json index 4f9a19f..ac7a90d 100644 --- a/config/locales/tr.json +++ b/config/locales/tr.json @@ -36,7 +36,10 @@ "filename": "Dosya adı:", "release_group": "Yayın grubu:", "upload_date": "Yükleme tarihi:", - "more_info": "Daha fazla bilgi" + "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", diff --git a/config/locales/zh.json b/config/locales/zh.json index 2da68ef..9d7f7d5 100644 --- a/config/locales/zh.json +++ b/config/locales/zh.json @@ -36,7 +36,10 @@ "filename": "文件名:", "release_group": "发布组:", "upload_date": "上传日期:", - "more_info": "更多信息" + "more_info": "更多信息", + "size": "大小:", + "old_experience": "正在使用旧版搜索体验。", + "new_experience": "正在使用新版搜索体验。" }, "about": { "title": "关于", @@ -88,6 +91,10 @@ "hide_non_game": { "label": "隐藏非游戏内容", "tooltip": "从搜索结果中过滤掉ROM修改、补丁、游戏插图等非游戏内容。" + }, + "use_old_results": { + "label": "旧版搜索", + "tooltip": "控制您的搜索是使用新版还是旧版结果体验。" } }, "save": "保存设置" @@ -114,7 +121,8 @@ "non_game": "警告:此文件可能不是游戏ROM,可能无法在网页模拟器中正常运行。", "see_about": "更多信息请参阅{{link}}页面。", "no_data": "没有可用的模拟器数据。", - "https": "不安全的HTTP连接:部分模拟器需要HTTPS才能正常运行。当前站点配置不正确。" + "https": "不安全的HTTP连接:部分模拟器需要HTTPS才能正常运行。当前站点配置不正确。", + "not_available_tooltip": "此标题无法使用网页模拟器,因为它不是游戏或平台不受支持。" }, "console": { "about": "这是一个在线模拟器,直接从Myrient的公共档案运行游戏ROM。", diff --git a/server.js b/server.js index b902ae1..ff0cdff 100644 --- a/server.js +++ b/server.js @@ -87,6 +87,7 @@ async function getFilesJob() { await metadataSearch.syncAllMetadata(); } optimizeDatabaseKws(); + //this is less important and needs to run last. metadataSearch.matchAllMetadata(true) } diff --git a/views/pages/results.ejs b/views/pages/results.ejs index 0a80e96..17fccca 100644 --- a/views/pages/results.ejs +++ b/views/pages/results.ejs @@ -30,6 +30,10 @@ <% } %> + + <%= __('search.new_experience') %> + +

                diff --git a/views/pages/resultsold.ejs b/views/pages/resultsold.ejs index b298a6a..d775a1b 100644 --- a/views/pages/resultsold.ejs +++ b/views/pages/resultsold.ejs @@ -36,6 +36,10 @@ <% } %> + + <%= __('search.old_experience') %> + +

                From 1a70611b10b978a406a0825131d727acb5250666 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 29 May 2025 17:58:34 -0600 Subject: [PATCH 35/46] update opengraph --- .env | 4 +++- docker-compose.yml | 1 + views/pages/index.ejs | 8 +++++++- views/partials/opengraph.ejs | 4 ++-- views/partials/opengraphinfo.ejs | 17 +++++++++++++++++ views/partials/opengraphresults.ejs | 4 ++-- 6 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 views/partials/opengraphinfo.ejs diff --git a/.env b/.env index 0e50335..340c0ed 100644 --- a/.env +++ b/.env @@ -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 @@ -27,4 +29,4 @@ 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= \ No newline at end of file +TWITCH_CLIENT_SECRET= diff --git a/docker-compose.yml b/docker-compose.yml index 60f7e27..1d2837b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/views/pages/index.ejs b/views/pages/index.ejs index e282313..a42c5fc 100644 --- a/views/pages/index.ejs +++ b/views/pages/index.ejs @@ -1,3 +1,9 @@ +<% + let ogPages = { + "results": "opengraphresults", + "info": "opengraphinfo" + } +%> @@ -6,7 +12,7 @@
                <%- include('../partials/header'); %> - <%- page == 'results' ? include('../partials/opengraphresults') : include('../partials/opengraph') %> + <%- ogPages[page] ? include(`../partials/${ogPages[page]}`) : include('../partials/opengraph') %>
                <%- include(page); %> diff --git a/views/partials/opengraph.ejs b/views/partials/opengraph.ejs index e988e94..0aadc79 100644 --- a/views/partials/opengraph.ejs +++ b/views/partials/opengraph.ejs @@ -1,7 +1,7 @@ - + @@ -10,7 +10,7 @@ - + diff --git a/views/partials/opengraphinfo.ejs b/views/partials/opengraphinfo.ejs new file mode 100644 index 0000000..98a809b --- /dev/null +++ b/views/partials/opengraphinfo.ejs @@ -0,0 +1,17 @@ + + + + + + +"> + + + + + + + + +"> + \ No newline at end of file diff --git a/views/partials/opengraphresults.ejs b/views/partials/opengraphresults.ejs index 74c20f4..8a839e7 100644 --- a/views/partials/opengraphresults.ejs +++ b/views/partials/opengraphresults.ejs @@ -9,7 +9,7 @@ resultString = resultString.trim() - + @@ -17,6 +17,6 @@ resultString = resultString.trim() - + From b430db07db151ee21d1394938286439dd88e80eb Mon Sep 17 00:00:00 2001 From: Alexandra Date: Thu, 29 May 2025 18:00:04 -0600 Subject: [PATCH 36/46] fix mistake on time format --- lib/time.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/time.js b/lib/time.js index 5275d38..16f8b33 100644 --- a/lib/time.js +++ b/lib/time.js @@ -9,7 +9,7 @@ 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"}`; } From bb9f99c11e92c622247860e574c8f381f8fa7f99 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Fri, 30 May 2025 01:26:29 -0600 Subject: [PATCH 37/46] Update metadata description translation --- config/locales/ar.json | 2 +- config/locales/bn.json | 2 +- config/locales/de.json | 2 +- config/locales/en.json | 2 +- config/locales/es.json | 2 +- config/locales/fr.json | 4 ++-- config/locales/hi.json | 2 +- config/locales/it.json | 2 +- config/locales/ja.json | 2 +- config/locales/ko.json | 2 +- config/locales/pl.json | 4 ++-- config/locales/pt.json | 2 +- config/locales/romaji.json | 2 +- config/locales/ru.json | 4 ++-- config/locales/tr.json | 4 ++-- config/locales/zh.json | 2 +- 16 files changed, 20 insertions(+), 20 deletions(-) diff --git a/config/locales/ar.json b/config/locales/ar.json index 45494dd..18ffee4 100644 --- a/config/locales/ar.json +++ b/config/locales/ar.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "معلومات البيانات الوصفية", - "description": "يقوم هذا الموقع بجلب معلومات البيانات الوصفية عن الألعاب من {{metadata_source}}. يتم جلب هذه المعلومات عند البحث. قد تكون عمليات البحث بطيئة حتى يتم تخزين البيانات الوصفية في قاعدة البيانات." + "description": "يقوم هذا الموقع بجلب معلومات البيانات الوصفية عن الألعاب من {{metadata_source}}. قد تكون بعض البيانات الوصفية مفقودة أو غير صحيحة بسبب عدم تطابق اسم ROM أو مشاكل مع مزود الخدمة." } }, "settings": { diff --git a/config/locales/bn.json b/config/locales/bn.json index 9ed5628..e7725a6 100644 --- a/config/locales/bn.json +++ b/config/locales/bn.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "মেটাডেটা তথ্য", - "description": "এই ওয়েবসাইটটি {{metadata_source}} থেকে গেমের মেটাডেটা তথ্য সংগ্রহ করে। এই তথ্য অনুসন্ধানের সময় সংগ্রহ করা হয়। মেটাডেটা ডাটাবেসে ক্যাশ না হওয়া পর্যন্ত অনুসন্ধান কুয়েরি ধীর হতে পারে।" + "description": "এই ওয়েবসাইট {{metadata_source}} থেকে গেমস সম্পর্কে মেটাডেটা তথ্য সংগ্রহ করে। ROM নামের অমিল বা পরিষেবা প্রদানকারীর সমস্যার কারণে কিছু মেটাডেটা অনুপস্থিত বা ভুল হতে পারে।" } }, "settings": { diff --git a/config/locales/de.json b/config/locales/de.json index cd916a6..69f9d77 100644 --- a/config/locales/de.json +++ b/config/locales/de.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "Metadaten-Informationen", - "description": "Diese Website bezieht Metadaten-Informationen über Spiele von {{metadata_source}}. Diese Informationen werden bei der Suche abgerufen. Suchanfragen können langsam sein, bis die Metadaten in der Datenbank zwischengespeichert sind." + "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": { diff --git a/config/locales/en.json b/config/locales/en.json index 0941fc5..22f1fc5 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "Metadata Information", - "description": "This website pulls metadata information about games from {{metadata_source}}. This information is pulled on search. Search queries may be slow until metadata has been cached in the database." + "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": { diff --git a/config/locales/es.json b/config/locales/es.json index 2e6a5ed..9adb77c 100644 --- a/config/locales/es.json +++ b/config/locales/es.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "Información de Metadatos", - "description": "Este sitio web obtiene información de metadatos sobre juegos de {{metadata_source}}. Esta información se obtiene durante la búsqueda. Las consultas de búsqueda pueden ser lentas hasta que los metadatos se almacenen en caché en la base de datos." + "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": { diff --git a/config/locales/fr.json b/config/locales/fr.json index e8ba29c..08780a5 100644 --- a/config/locales/fr.json +++ b/config/locales/fr.json @@ -60,8 +60,8 @@ "view_github": "Voir le projet sur GitHub" }, "metadata": { - "title": "Informations sur les métadonnées", - "description": "Ce site web récupère les métadonnées des jeux depuis {{metadata_source}}. Ces informations sont récupérées lors de la recherche. Les requêtes de recherche peuvent être lentes jusqu'à ce que les métadonnées soient mises en cache dans la base de données." + "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": { diff --git a/config/locales/hi.json b/config/locales/hi.json index 780fe0e..3b30360 100644 --- a/config/locales/hi.json +++ b/config/locales/hi.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "मेटाडेटा जानकारी", - "description": "यह वेबसाइट {{metadata_source}} से गेम्स की मेटाडेटा जानकारी प्राप्त करती है। यह जानकारी खोज के समय प्राप्त की जाती है। मेटाडेटा डेटाबेस में कैश होने तक खोज क्वेरी धीमी हो सकती हैं।" + "description": "यह वेबसाइट {{metadata_source}} से गेम्स के बारे में मेटाडेटा जानकारी प्राप्त करती है। ROM नाम में असंगतता या सेवा प्रदाता की समस्याओं के कारण कुछ मेटाडेटा अनुपलब्ध या गलत हो सकता है।" } }, "settings": { diff --git a/config/locales/it.json b/config/locales/it.json index 21d4df7..6c72755 100644 --- a/config/locales/it.json +++ b/config/locales/it.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "Informazioni sui Metadati", - "description": "Questo sito web recupera informazioni sui metadati dei giochi da {{metadata_source}}. Queste informazioni vengono recuperate durante la ricerca. Le query di ricerca potrebbero essere lente fino a quando i metadati non vengono memorizzati nella cache del database." + "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": { diff --git a/config/locales/ja.json b/config/locales/ja.json index ff6423c..872966e 100644 --- a/config/locales/ja.json +++ b/config/locales/ja.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "メタデータ情報", - "description": "このサイトは{{metadata_source}}からゲームのメタデータ情報を取得します。この情報は検索時に取得されます。メタデータがデータベースにキャッシュされるまで、検索クエリは遅くなる可能性があります。" + "description": "このウェブサイトは {{metadata_source}} からゲームのメタデータ情報を取得しています。ROMの名前の不一致やサービスプロバイダーの問題により、一部のメタデータが欠落しているか、不正確な場合があります。" } }, "settings": { diff --git a/config/locales/ko.json b/config/locales/ko.json index a69d442..506a283 100644 --- a/config/locales/ko.json +++ b/config/locales/ko.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "메타데이터 정보", - "description": "이 웹사이트는 {{metadata_source}}에서 게임의 메타데이터 정보를 가져옵니다. 이 정보는 검색 시 가져옵니다. 메타데이터가 데이터베이스에 캐시될 때까지 검색 쿼리가 느릴 수 있습니다." + "description": "이 웹사이트는 {{metadata_source}}에서 게임의 메타데이터 정보를 가져옵니다. ROM 이름의 불일치나 서비스 제공자의 문제로 인해 일부 메타데이터가 누락되거나 부정확할 수 있습니다." } }, "settings": { diff --git a/config/locales/pl.json b/config/locales/pl.json index d7a2e84..283686a 100644 --- a/config/locales/pl.json +++ b/config/locales/pl.json @@ -60,8 +60,8 @@ "view_github": "Zobacz projekt na GitHub" }, "metadata": { - "title": "Informacje o metadanych", - "description": "Ta strona pobiera informacje o metadanych gier z {{metadata_source}}. Te informacje są pobierane podczas wyszukiwania. Zapytania wyszukiwania mogą być wolne, dopóki metadane nie zostaną zapisane w pamięci podręcznej bazy danych." + "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": { diff --git a/config/locales/pt.json b/config/locales/pt.json index e8a7a4b..f046a26 100644 --- a/config/locales/pt.json +++ b/config/locales/pt.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "Informações de Metadados", - "description": "Este site obtém informações de metadados sobre jogos do {{metadata_source}}. Essas informações são obtidas durante a pesquisa. As consultas de pesquisa podem ser lentas até que os metadados sejam armazenados em cache no banco de dados." + "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": { diff --git a/config/locales/romaji.json b/config/locales/romaji.json index e300ea8..c956d14 100644 --- a/config/locales/romaji.json +++ b/config/locales/romaji.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "Metadēta Jōhō", - "description": "Kono saito wa {{metadata_source}} kara gēmu no metadēta jōhō wo shutoku shimasu. Kono jōhō wa kensaku-ji ni shutoku saremasu. Metadēta ga dētabēsu ni kyasshu sareru made, kensaku kueri wa osoku naru kanōsei ga arimasu." + "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": { diff --git a/config/locales/ru.json b/config/locales/ru.json index 4812af6..fb2a8b1 100644 --- a/config/locales/ru.json +++ b/config/locales/ru.json @@ -60,8 +60,8 @@ "view_github": "Посмотреть проект на GitHub" }, "metadata": { - "title": "Информация о метаданных", - "description": "Этот сайт получает метаданные об играх из {{metadata_source}}. Эта информация загружается при поиске. Поисковые запросы могут быть медленными, пока метаданные не будут кэшированы в базе данных." + "title": "Информация о Метаданных", + "description": "Этот веб-сайт получает метаданные об играх из {{metadata_source}}. Некоторые метаданные могут отсутствовать или быть неточными из-за несоответствий в названиях ROM или проблем с поставщиком услуг." } }, "settings": { diff --git a/config/locales/tr.json b/config/locales/tr.json index ac7a90d..dc548fa 100644 --- a/config/locales/tr.json +++ b/config/locales/tr.json @@ -60,8 +60,8 @@ "view_github": "Projeyi GitHub'da görüntüle" }, "metadata": { - "title": "Meta Veri Bilgisi", - "description": "Bu web sitesi {{metadata_source}} üzerinden oyunlar hakkında meta veri bilgisi çeker. Bu bilgiler arama sırasında çekilir. Meta veriler veritabanında önbelleğe alınana kadar arama sorguları yavaş olabilir." + "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": { diff --git a/config/locales/zh.json b/config/locales/zh.json index 9d7f7d5..6531039 100644 --- a/config/locales/zh.json +++ b/config/locales/zh.json @@ -61,7 +61,7 @@ }, "metadata": { "title": "元数据信息", - "description": "本网站从{{metadata_source}}获取游戏的元数据信息。此信息在搜索时获取。在元数据缓存到数据库之前,搜索查询可能会较慢。" + "description": "本网站从 {{metadata_source}} 获取游戏的元数据信息。由于 ROM 名称不匹配或服务提供商的问题,某些元数据可能缺失或不准确。" } }, "settings": { From 84908e0882becc9284b5cff6226d1e4de668894e Mon Sep 17 00:00:00 2001 From: Alexandra Date: Fri, 30 May 2025 03:21:24 -0600 Subject: [PATCH 38/46] Fixes a few more things with localizations and a couple of programming errors. --- config/locales/fr.json | 2 +- config/locales/ja.json | 2 +- config/locales/ko.json | 2 +- config/locales/pt.json | 2 +- config/locales/romaji.json | 2 +- config/locales/zh.json | 2 +- lib/database.js | 36 ++++++++++++++++++++++++++-- lib/json/maps/name_localization.json | 2 +- lib/metadatasearch.js | 10 ++++---- lib/search.js | 9 +++++-- server.js | 11 +++++---- views/pages/info.ejs | 12 ++++------ views/partials/opengraphinfo.ejs | 8 ++++++- views/partials/result.ejs | 6 ++--- 14 files changed, 75 insertions(+), 31 deletions(-) diff --git a/config/locales/fr.json b/config/locales/fr.json index 08780a5..15d5a86 100644 --- a/config/locales/fr.json +++ b/config/locales/fr.json @@ -32,7 +32,7 @@ "published": "Publié par :", "developed": "Développé par :", "modes": "Modes de jeu :", - "download": "Télécharger :", + "download": "Télécharger", "filename": "Nom du fichier :", "release_group": "Groupe de release :", "upload_date": "Date de téléchargement :", diff --git a/config/locales/ja.json b/config/locales/ja.json index 872966e..9634901 100644 --- a/config/locales/ja.json +++ b/config/locales/ja.json @@ -32,7 +32,7 @@ "published": "発売元:", "developed": "開発元:", "modes": "ゲームモード:", - "download": "ダウンロード:", + "download": "ダウンロード", "filename": "ファイル名:", "release_group": "リリースグループ:", "upload_date": "アップロード日:", diff --git a/config/locales/ko.json b/config/locales/ko.json index 506a283..fdae9aa 100644 --- a/config/locales/ko.json +++ b/config/locales/ko.json @@ -32,7 +32,7 @@ "published": "퍼블리셔:", "developed": "개발사:", "modes": "게임 모드:", - "download": "다운로드:", + "download": "다운로드", "filename": "파일명:", "release_group": "릴리즈 그룹:", "upload_date": "업로드 날짜:", diff --git a/config/locales/pt.json b/config/locales/pt.json index f046a26..76ce834 100644 --- a/config/locales/pt.json +++ b/config/locales/pt.json @@ -32,7 +32,7 @@ "published": "Publicado por:", "developed": "Desenvolvido por:", "modes": "Modos de jogo:", - "download": "Baixar:", + "download": "Baixar", "filename": "Nome do arquivo:", "release_group": "Grupo de lançamento:", "upload_date": "Data de upload:", diff --git a/config/locales/romaji.json b/config/locales/romaji.json index c956d14..f9485d7 100644 --- a/config/locales/romaji.json +++ b/config/locales/romaji.json @@ -32,7 +32,7 @@ "published": "Hatsubaisha:", "developed": "Kaihatsusha:", "modes": "Gēmu mōdo:", - "download": "Daunrōdo:", + "download": "Daunrōdo", "filename": "Fairu mei:", "release_group": "Rirīsu gurūpu:", "upload_date": "Apurōdo-bi:", diff --git a/config/locales/zh.json b/config/locales/zh.json index 6531039..8eef551 100644 --- a/config/locales/zh.json +++ b/config/locales/zh.json @@ -32,7 +32,7 @@ "published": "发行商:", "developed": "开发商:", "modes": "游戏模式:", - "download": "下载:", + "download": "下载", "filename": "文件名:", "release_group": "发布组:", "upload_date": "上传日期:", diff --git a/lib/database.js b/lib/database.js index 035d69a..395ac39 100644 --- a/lib/database.js +++ b/lib/database.js @@ -26,14 +26,46 @@ Metadata.hasMany(File); File.belongsTo(Metadata, { as: "details" }); async function enableTrigram() { - const query = `SELECT * from PG_extension where extname = 'pg_trgm'`; - const [result] = await sequelize.query(query, { + 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() { diff --git a/lib/json/maps/name_localization.json b/lib/json/maps/name_localization.json index 06f7ac2..fea3cbb 100644 --- a/lib/json/maps/name_localization.json +++ b/lib/json/maps/name_localization.json @@ -13,6 +13,6 @@ "fr": "French", "ja": "Japan", "pt": "Portuguese", - "romaji": "Romaji", + "romaji": "Japanese title - romanization", "zh": "Chinese" } \ No newline at end of file diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index eeda3a9..12563e9 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -66,7 +66,7 @@ export default class MetadataSearch { this.platformMap = JSON.parse(readFileSync(mapFilePath, "utf8")); if (this.accessToken) { this.authorized = true; - this.matchAllMetadata(); + this.syncAllMetadata() return; } } @@ -114,7 +114,7 @@ export default class MetadataSearch { let games = await File.findAndCountAll({ where: { nongame: false, - //detailsId: null, + detailsId: null, }, limit: 1000, order: ["id", "filename"], @@ -128,7 +128,7 @@ export default class MetadataSearch { games = await File.findAndCountAll({ where: { nongame: false, - //detailsId: null, + detailsId: null, }, limit: 1000, offset: x * 1000, @@ -158,7 +158,7 @@ export default class MetadataSearch { //this is much slower and should only be used if the faster full text search can't find it. let metadata = await Metadata.fuzzySearchByText( this.normalizeName(game.filename), - 0.6, + 0.8, game.category ); if (metadata.length) { @@ -300,7 +300,7 @@ export default class MetadataSearch { } } //this needs to remain json as we want the keys to be retained - md.alternatetitles = alternates.length + md.alternatetitles = alternates ? JSON.stringify(alternates) : null; await md.save(); diff --git a/lib/search.js b/lib/search.js index fbdda17..e1dae80 100644 --- a/lib/search.js +++ b/lib/search.js @@ -3,7 +3,7 @@ import { search as elasticSearch, getSuggestions as elasticSuggestions, } from "./services/elasticsearch.js"; -import { File } from "./models/index.js"; +import { File, Metadata } from "./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() { diff --git a/server.js b/server.js index ff0cdff..eddf250 100644 --- a/server.js +++ b/server.js @@ -332,14 +332,17 @@ app.get("/info/:id", async function (req, res) { } let romId = parseInt(req.params.id); let romFile = await search.findIndex(romId); - let romInfo = await metadataSearch.queueGetGamesMetadata([romFile]); - - if (!romInfo.length) { + if (!romFile) { res.redirect("/"); return; } let options = { - romFile: romInfo[0], + file: { + ...romFile.dataValues + }, + metadata: { + ...romFile?.details?.dataValues + }, flags: flags, consoleIcons: consoleIcons, localeNames: localeNames diff --git a/views/pages/info.ejs b/views/pages/info.ejs index 9fc3e13..165f190 100644 --- a/views/pages/info.ejs +++ b/views/pages/info.ejs @@ -1,24 +1,22 @@ <% - const metadata = romFile.metadata || new Object() - const file = romFile.file || new Object() - const titles = JSON.parse(metadata.alternatetitles) + 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(titles[x].includes(localeNames[lang])){ + if(x.includes(localeNames[locale])){ title = titles[x] } } - const coverarts = JSON.parse(metadata.coverartid) + 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 = JSON.parse(metadata.screenshots).map((im) => `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_720p/${im}.webp`) + 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 = JSON.parse(metadata.videos) + videos = metadata.videos } %> diff --git a/views/partials/opengraphinfo.ejs b/views/partials/opengraphinfo.ejs index 98a809b..b409792 100644 --- a/views/partials/opengraphinfo.ejs +++ b/views/partials/opengraphinfo.ejs @@ -1,3 +1,9 @@ +<% +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" +%> + @@ -12,6 +18,6 @@ - + "> \ No newline at end of file diff --git a/views/partials/result.ejs b/views/partials/result.ejs index 7e34cb2..505705e 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -1,15 +1,15 @@ <% const metadata = result.metadata || new Object() const file = result.file || new Object() - const titles = JSON.parse(metadata.alternatetitles) + 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(titles[x].includes(localeNames[lang])){ + if(x.includes(localeNames[locale])){ title = titles[x] } } - const coverarts = JSON.parse(metadata.coverartid) + 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" %> From 0457533f76ce67dff6c98be0d040af3d8be58952 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Fri, 30 May 2025 04:26:40 -0600 Subject: [PATCH 39/46] fix suggestions fix info title page remove carousel for games without applicable metadata --- lib/dboptimize.js | 2 +- lib/dircrawl.js | 4 ++-- lib/metadatasearch.js | 16 +++++++------ lib/services/elasticsearch.js | 43 +++++++++++++++++++++++++++-------- server.js | 12 +++++----- views/pages/info.ejs | 4 +++- 6 files changed, 54 insertions(+), 27 deletions(-) diff --git a/lib/dboptimize.js b/lib/dboptimize.js index a603b8f..0ab9d3c 100644 --- a/lib/dboptimize.js +++ b/lib/dboptimize.js @@ -38,7 +38,7 @@ export async function optimizeDatabaseKws() { singleLineStatus( `Optimizing Keywords: ${i} / ${dbLength} ${((i / dbLength) * 100).toFixed( 2 - )} (${proctime.elapsed()})}` + )}% (${proctime.elapsed()})` ); let result = await File.findAndCountAll({ limit: BATCH_SIZE, diff --git a/lib/dircrawl.js b/lib/dircrawl.js index 4939cc8..7381b7a 100644 --- a/lib/dircrawl.js +++ b/lib/dircrawl.js @@ -95,7 +95,7 @@ export default async function getAllFiles(catList) { fetchTasks = []; dirStatus = `Directories Remaining: ${ dirs.length - }, Files Found: ${fileCount} (${proctime.elapsed()}`; + }, Files Found: ${fileCount} (${proctime.elapsed()})`; } if (dirs.length == 0 && parseTasks.length > 0) { @@ -125,7 +125,7 @@ export default async function getAllFiles(catList) { parseTasks = []; dirStatus = `Directories Remaining: ${ dirs.length - }, Files Found: ${fileCount} (${proctime.elapsed()}`; + }, Files Found: ${fileCount} (${proctime.elapsed()})`; } if (dirStatus) { diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 12563e9..399d20a 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -55,8 +55,6 @@ export default class MetadataSearch { "videos.video_id", ]; - getPlatformMapping() {} - async setupClient() { try { if (this.twitchSecrets.client_id && this.twitchSecrets.client_secret) { @@ -66,12 +64,14 @@ export default class MetadataSearch { this.platformMap = JSON.parse(readFileSync(mapFilePath, "utf8")); if (this.accessToken) { this.authorized = true; - this.syncAllMetadata() + this.ready = true; return; } } + this.ready = true; this.authorized = false; //disable } catch (error) { + this.ready = true; this.authorized = false; } } @@ -89,7 +89,11 @@ export default class MetadataSearch { async getIGDBGamesCount(retrying = false) { try { - if (!this.authorized) return 0; + // 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( @@ -300,9 +304,7 @@ export default class MetadataSearch { } } //this needs to remain json as we want the keys to be retained - md.alternatetitles = alternates - ? JSON.stringify(alternates) - : null; + md.alternatetitles = alternates ? JSON.stringify(alternates) : null; await md.save(); } catch (error) { console.error("Error adding metadata:", error); diff --git a/lib/services/elasticsearch.js b/lib/services/elasticsearch.js index fe58409..aaa456a 100644 --- a/lib/services/elasticsearch.js +++ b/lib/services/elasticsearch.js @@ -250,21 +250,33 @@ export async function getSuggestions(query, options) { index: INDEX_NAME, body: { query: { - multi_match: { - query, - fields: ["filename^2", "filenamekws^2", "category", "categorykws"], - fuzziness: "AUTO", - type: "best_fields", - }, + bool:{ + must: { + multi_match: { + query, + fields: ["filename^2", "filenamekws^2", "category", "categorykws"], + fuzziness: "AUTO", + type: "best_fields", + }, + }, + filter: { + term:{ + nongame: false + } + } + } }, _source: ["filename", "category"], - size: 10, + 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); return []; @@ -294,3 +306,14 @@ export async function getSample(query, options) { 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(); + } \ No newline at end of file diff --git a/server.js b/server.js index eddf250..87c5120 100644 --- a/server.js +++ b/server.js @@ -83,10 +83,10 @@ async function getFilesJob() { } crawlTime = Date.now(); console.log(`Finished updating file list. ${fileCount} found.`); - if(Metadata.count() > metadataSearch.getIGDBGamesCount()){ + if(await Metadata.count() < await metadataSearch.getIGDBGamesCount()){ await metadataSearch.syncAllMetadata(); } - optimizeDatabaseKws(); + await optimizeDatabaseKws(); //this is less important and needs to run last. metadataSearch.matchAllMetadata(true) } @@ -213,11 +213,11 @@ app.get("/search", async function (req, res) { delete settings.combineWith; } let loadOldResults = - req.query.old === "true" || !Metadata.count() ? true : false; + 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, settings); + let results = await search.findAllMatches(query.trim(), settings); debugPrint(results); if (results.count && pageNum == 1) { queryCount += 1; @@ -268,10 +268,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 = Metadata.count() ? true : false; + options.oldSettingsAvailable = await Metadata.count() ? true : false; options = buildOptions(page, options); res.render(indexPage, options); }); diff --git a/views/pages/info.ejs b/views/pages/info.ejs index 165f190..20cf37c 100644 --- a/views/pages/info.ejs +++ b/views/pages/info.ejs @@ -24,7 +24,7 @@
                -

                <%= title %>

                +

                <%= title || file.filename %>

                <%= file.category %> <%- consoleIcons.createConsoleImage(file.category) %>

                @@ -107,9 +107,11 @@
                + <% if(images.length || videos.length){ %>
                <%- include("../partials/carousel", {images: images, videos: videos})%>
                + <% } %>
                From 634747e18c880d30abec4ea071f5e734100da585 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Fri, 30 May 2025 04:56:49 -0600 Subject: [PATCH 40/46] make sync wait for match to finish --- lib/metadatasearch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 399d20a..11ca207 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -231,7 +231,7 @@ export default class MetadataSearch { retryCount = 0; } console.log(`\nFinished syncing metadata in ${timer.elapsed()}`); - this.matchAllMetadata(); + await this.matchAllMetadata(); } catch (error) { if (error.code === "ERR_BAD_REQUEST" && !retrying) { this.setupClient(); From 5c5aa236e45aa84e7bc9c64f12815b19ba177c66 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Fri, 30 May 2025 05:18:19 -0600 Subject: [PATCH 41/46] add further control for metadata update update readme with new information --- README.md | 9 +++++++++ lib/metadatasearch.js | 1 - server.js | 8 +++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f83895b..3c076c8 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 11ca207..a0ad724 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -231,7 +231,6 @@ export default class MetadataSearch { retryCount = 0; } console.log(`\nFinished syncing metadata in ${timer.elapsed()}`); - await this.matchAllMetadata(); } catch (error) { if (error.code === "ERR_BAD_REQUEST" && !retrying) { this.setupClient(); diff --git a/server.js b/server.js index 87c5120..0610b5f 100644 --- a/server.js +++ b/server.js @@ -76,6 +76,7 @@ let metadataSearch = new MetadataSearch(); async function getFilesJob() { console.log("Updating the file list."); + let oldFileCount = fileCount || 0 fileCount = await getAllFiles(categoryList); if (!fileCount) { console.log("File update failed"); @@ -86,9 +87,14 @@ async function getFilesJob() { if(await Metadata.count() < await metadataSearch.getIGDBGamesCount()){ await metadataSearch.syncAllMetadata(); } + if(fileCount > oldFileCount){ + await metadataSearch.matchAllMetadata() + } await optimizeDatabaseKws(); //this is less important and needs to run last. - metadataSearch.matchAllMetadata(true) + if(fileCount > oldFileCount){ + metadataSearch.matchAllMetadata(true) + } } function buildOptions(page, options) { From e7bfcdc87a4bbaffd8e615b494ce147ed5817eb0 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Fri, 30 May 2025 06:52:24 -0600 Subject: [PATCH 42/46] separate alternate titles and titles metadata as it was dropping the quality of matches --- lib/metadatasearch.js | 32 +++++++++++++++++++++++----- lib/models/metadata.js | 48 +++++++++++++++++++++++++++++------------- server.js | 3 +++ 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index a0ad724..bc7bc9c 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -90,8 +90,8 @@ export default class MetadataSearch { async getIGDBGamesCount(retrying = false) { try { // hack to ensure the client is ready before we do anything - while(!this.ready){ - await this.sleep(500) + while (!this.ready) { + await this.sleep(500); } if (this.authorized === false) return 0; const { data } = await this.client @@ -118,7 +118,7 @@ export default class MetadataSearch { let games = await File.findAndCountAll({ where: { nongame: false, - detailsId: null, + //detailsId: null, }, limit: 1000, order: ["id", "filename"], @@ -132,10 +132,10 @@ export default class MetadataSearch { games = await File.findAndCountAll({ where: { nongame: false, - detailsId: null, + //detailsId: null, }, limit: 1000, - offset: x * 1000, + offset: x * 1000 - found, order: ["id", "filename"], include: { model: Metadata, as: "details" }, }); @@ -148,9 +148,18 @@ export default class MetadataSearch { ); let game = games.rows[y]; let metadata = await Metadata.searchByText( + "title", this.normalizeName(game.filename), game.category ); + if (!metadata.length) { + // repeat the search under one of the alternate titles + metadata = await Metadata.searchByText( + "alternatetitles", + this.normalizeName(game.filename), + game.category + ); + } if (metadata.length) { let md = await Metadata.findByPk(metadata[0].id); await game.setDetails(md); @@ -161,10 +170,19 @@ export default class MetadataSearch { } 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) { + metadata = await Metadata.fuzzySearchByText( + "alternatetitles", + this.normalizeName(game.filename), + 0.8, + game.category + ); + } if (metadata.length) { let md = await Metadata.findByPk(metadata[0].id); await game.setDetails(md); @@ -182,6 +200,10 @@ export default class MetadataSearch { 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." diff --git a/lib/models/metadata.js b/lib/models/metadata.js index d5fd278..6a10fed 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -49,7 +49,11 @@ export default function (sequelize) { videos: { type: DataTypes.ARRAY(DataTypes.STRING), }, - searchVector: { + titleVector: { + type: DataTypes.TSVECTOR, + allowNull: true, + }, + alternatetitlesVector: { type: DataTypes.TSVECTOR, allowNull: true, }, @@ -58,9 +62,14 @@ export default function (sequelize) { indexes: [ { fields: ["title"] }, { - name: "metadata_search_idx", + name: "metadata_search_t_idx", using: "gin", - fields: ["searchVector"], + fields: ["titleVector"], + }, + { + name: "metadata_search_at_idx", + using: "gin", + fields: ["alternatetitlesVector"], }, ], } @@ -71,36 +80,45 @@ export default function (sequelize) { const alternateTitles = JSON.parse(instance.alternatetitles || "[]") const titles = Object.values(alternateTitles).join(', ') - const query = ` - SELECT to_tsvector('english', $1 || ', ' || $2) + let query = ` + SELECT to_tsvector('english', $1) `; - const [results] = await sequelize.query(query, { - bind: [title, titles], + let [results] = await sequelize.query(query, { + bind: [title], raw: true, }); - instance.searchVector = results[0].to_tsvector; + 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 (searchQuery, platform, limit = 1) { + Metadata.searchByText = async function (field, searchQuery, platform, limit = 1) { let platformClause = ""; let limitClause = `LIMIT ${limit}`; - if (platform) { + if (platform && platform != "Others") { platformClause = `AND '${platform}' = ANY(platforms)`; } const query = ` SELECT id FROM "Metadata" - WHERE "searchVector" @@ plainto_tsquery('english', $1) ${platformClause} + WHERE $1 @@ plainto_tsquery('english', $2) ${platformClause} ORDER BY length(title) ${limitClause} `; return await sequelize.query(query, { model: Metadata, - bind: [searchQuery], + bind: [(field + 'Vector'), searchQuery], type: sequelize.QueryTypes.SELECT, }); }; Metadata.fuzzySearchByText = async function ( + field, searchQuery, fuzziness, platform, @@ -109,19 +127,19 @@ export default function (sequelize) { fuzziness = fuzziness || 0.6; let platformClause = ""; let limitClause = `LIMIT ${limit}`; - if (platform) { + if (platform && platform != "Others") { platformClause = `AND '${platform}' = ANY(platforms)`; } const query = ` SELECT id FROM "Metadata" - WHERE SIMILARITY(title, $1) > $2 OR SIMILARITY(alternatetitles, $1) > $2 + WHERE SIMILARITY($1, $2) > $3 ${platformClause} ORDER BY length(title) ${limitClause} `; return await sequelize.query(query, { model: Metadata, - bind: [searchQuery, fuzziness], + bind: [field, searchQuery, fuzziness], type: sequelize.QueryTypes.SELECT, }); }; diff --git a/server.js b/server.js index 0610b5f..7fef03c 100644 --- a/server.js +++ b/server.js @@ -578,3 +578,6 @@ if ( } cron.schedule("0 30 2 * * *", getFilesJob); + +await metadataSearch.syncAllMetadata() +await metadataSearch.matchAllMetadata() \ No newline at end of file From d0d8ba01aa525cd5b50ea6e2e46fd8a7443e52a7 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Fri, 30 May 2025 17:15:08 -0600 Subject: [PATCH 43/46] Remove sequelize pagination in full db tasks cause uh... it has a lot of problems that make no sense to me. --- lib/dboptimize.js | 45 ++++++++++++++------ lib/metadatasearch.js | 96 ++++++++++++++++++------------------------ lib/models/metadata.js | 6 +-- server.js | 4 +- 4 files changed, 80 insertions(+), 71 deletions(-) diff --git a/lib/dboptimize.js b/lib/dboptimize.js index 0ab9d3c..95a0b22 100644 --- a/lib/dboptimize.js +++ b/lib/dboptimize.js @@ -34,22 +34,38 @@ export async function optimizeDatabaseKws() { let dbLength = await File.count(); let optimizeTasks = []; let resolvedTasks = []; + 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 i = 0; i < dbLength; ) { + let loopIndexStart = i; singleLineStatus( `Optimizing Keywords: ${i} / ${dbLength} ${((i / dbLength) * 100).toFixed( 2 )}% (${proctime.elapsed()})` ); - let result = await File.findAndCountAll({ - limit: BATCH_SIZE, - offset: i, - order: ["id", "filename"], - }); - for (let x = 0; x < result.rows.length; x++) { - debugPrint(`Submitting job for: ${result.rows[x]["filename"]}`); + + 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 @@ -66,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(); + result[promiseIndex].save(); changes++; } + promiseIndex++; } - await bulkIndexFiles(result.rows); + await bulkIndexFiles(result.slice(loopIndexStart, currentIndex)); optimizeTasks = []; resolvedTasks = []; } diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index bc7bc9c..a6d8bd7 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -115,82 +115,70 @@ export default class MetadataSearch { } async matchAllMetadata(fuzzy = false) { - let games = await File.findAndCountAll({ + let games = await File.findAll({ where: { nongame: false, - //detailsId: null, + detailsId: null, }, - limit: 1000, + attributes: ["id", "filename"], order: ["id", "filename"], + include: { model: Metadata, as: "details" }, }); - let count = games.count; - let pages = Math.ceil(games.count / 1000); + let count = games.length; let timer = new Timer(); let found = 0; console.log(`Matching ${count} games to metadata.`); - for (let x = 0; x < pages; x++) { - games = await File.findAndCountAll({ - where: { - nongame: false, - //detailsId: null, - }, - limit: 1000, - offset: x * 1000 - found, - order: ["id", "filename"], - include: { model: Metadata, as: "details" }, - }); - for (let y = 0; y < games.rows.length; y++) { - singleLineStatus( - `Matching metadata: ${x * 1000 + y} / ${count} ${( - ((x * 1000 + y) / count) * - 100 - ).toFixed(2)}% (${timer.elapsed()}) Total Matches: ${found}` - ); - let game = games.rows[y]; - let metadata = await Metadata.searchByText( - "title", + 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) { - // repeat the search under one of the alternate titles - metadata = await Metadata.searchByText( + } + 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) { + 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) { - metadata = await Metadata.fuzzySearchByText( - "alternatetitles", - this.normalizeName(game.filename), - 0.8, - game.category - ); - } - if (metadata.length) { - let md = await Metadata.findByPk(metadata[0].id); - await game.setDetails(md); - await md.addFile(game); - await game.save(); - await md.save(); - found++; - } } } } diff --git a/lib/models/metadata.js b/lib/models/metadata.js index 6a10fed..e914c4f 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -105,14 +105,14 @@ export default function (sequelize) { if (platform && platform != "Others") { platformClause = `AND '${platform}' = ANY(platforms)`; } + let fieldName = field + 'Vector' const query = ` SELECT id FROM "Metadata" - WHERE $1 @@ plainto_tsquery('english', $2) ${platformClause} + WHERE "${fieldName}" @@ plainto_tsquery('english', $1) ${platformClause} ORDER BY length(title) ${limitClause} `; return await sequelize.query(query, { - model: Metadata, - bind: [(field + 'Vector'), searchQuery], + bind: [searchQuery], type: sequelize.QueryTypes.SELECT, }); }; diff --git a/server.js b/server.js index 7fef03c..9a69495 100644 --- a/server.js +++ b/server.js @@ -40,6 +40,7 @@ let indexPage = "pages/index"; let flags = new Flag(); let consoleIcons = new ConsoleIcons(emulatorsData); + // Initialize databases await initDB(); await initElasticsearch(); @@ -579,5 +580,4 @@ if ( cron.schedule("0 30 2 * * *", getFilesJob); -await metadataSearch.syncAllMetadata() -await metadataSearch.matchAllMetadata() \ No newline at end of file +await optimizeDatabaseKws() \ No newline at end of file From 80214b5317b2fbf66cf2814fc32ebefba90d3c45 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Fri, 30 May 2025 17:43:55 -0600 Subject: [PATCH 44/46] remove testing bs --- server.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server.js b/server.js index 9a69495..0f89045 100644 --- a/server.js +++ b/server.js @@ -580,4 +580,3 @@ if ( cron.schedule("0 30 2 * * *", getFilesJob); -await optimizeDatabaseKws() \ No newline at end of file From 96449a48174a6d91a901c77ba547fdbfa5c163d2 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Sat, 31 May 2025 13:53:13 -0600 Subject: [PATCH 45/46] reorganize project, fix a couple of miscellaneous issues. --- lib/{ => crawler}/dircrawl.js | 12 ++-- lib/{ => crawler}/filehandler.js | 0 .../metadatamanager.js} | 10 +-- lib/{ => crawler/workers}/fileworker.js | 2 +- lib/{ => database}/database.js | 3 + lib/{ => database}/dboptimize.js | 16 ++--- lib/{ => database}/models/file.js | 0 lib/{ => database}/models/index.js | 0 lib/{ => database}/models/metadata.js | 0 lib/{ => database}/models/queryCount.js | 0 lib/{ => database/workers}/dbkwworker.js | 2 +- lib/{ => emulator}/emulatorConfig.js | 2 +- lib/{ => images}/consoleicons.js | 0 lib/{ => images}/flag.js | 2 +- lib/{ => json/dynamic_content}/emulators.json | 0 lib/{ => json/terms}/categories.json | 0 lib/{ => json/terms}/nonGameTerms.json | 0 lib/{ => search}/search.js | 6 +- lib/services/elasticsearch.js | 6 +- lib/{ => utility}/asciiart.js | 0 lib/{debugprint.js => utility/printutils.js} | 0 lib/{ => utility}/taskqueue.js | 0 lib/{ => utility}/time.js | 0 server.js | 66 +++++++++---------- 24 files changed, 62 insertions(+), 65 deletions(-) rename lib/{ => crawler}/dircrawl.js (94%) rename lib/{ => crawler}/filehandler.js (100%) rename lib/{metadatasearch.js => crawler/metadatamanager.js} (97%) rename lib/{ => crawler/workers}/fileworker.js (98%) rename lib/{ => database}/database.js (96%) rename lib/{ => database}/dboptimize.js (89%) rename lib/{ => database}/models/file.js (100%) rename lib/{ => database}/models/index.js (100%) rename lib/{ => database}/models/metadata.js (100%) rename lib/{ => database}/models/queryCount.js (100%) rename lib/{ => database/workers}/dbkwworker.js (98%) rename lib/{ => emulator}/emulatorConfig.js (99%) rename lib/{ => images}/consoleicons.js (100%) rename lib/{ => images}/flag.js (92%) rename lib/{ => json/dynamic_content}/emulators.json (100%) rename lib/{ => json/terms}/categories.json (100%) rename lib/{ => json/terms}/nonGameTerms.json (100%) rename lib/{ => search}/search.js (83%) rename lib/{ => utility}/asciiart.js (100%) rename lib/{debugprint.js => utility/printutils.js} (100%) rename lib/{ => utility}/taskqueue.js (100%) rename lib/{ => utility}/time.js (100%) diff --git a/lib/dircrawl.js b/lib/crawler/dircrawl.js similarity index 94% rename from lib/dircrawl.js rename to lib/crawler/dircrawl.js index 7381b7a..9b977f4 100644 --- a/lib/dircrawl.js +++ b/lib/crawler/dircrawl.js @@ -1,13 +1,13 @@ -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 { 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(), }); diff --git a/lib/filehandler.js b/lib/crawler/filehandler.js similarity index 100% rename from lib/filehandler.js rename to lib/crawler/filehandler.js diff --git a/lib/metadatasearch.js b/lib/crawler/metadatamanager.js similarity index 97% rename from lib/metadatasearch.js rename to lib/crawler/metadatamanager.js index a6d8bd7..8b326ef 100644 --- a/lib/metadatasearch.js +++ b/lib/crawler/metadatamanager.js @@ -16,13 +16,13 @@ import { limit, offset, } from "@phalcode/ts-igdb-client"; -import { File, Metadata } from "./database.js"; -import TaskQueue from "./taskqueue.js"; -import { singleLineStatus } from "./debugprint.js"; -import { Timer } from "./time.js"; +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 MetadataSearch { +export default class MetadataManager { constructor() { this.twitchSecrets = { client_id: process.env.TWITCH_CLIENT_ID, diff --git a/lib/fileworker.js b/lib/crawler/workers/fileworker.js similarity index 98% rename from lib/fileworker.js rename to lib/crawler/workers/fileworker.js index ebfceb1..1843523 100644 --- a/lib/fileworker.js +++ b/lib/crawler/workers/fileworker.js @@ -181,7 +181,7 @@ function getNonGameTerms() { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); - const nonGameTermsPath = resolve(__dirname, 'nonGameTerms.json'); + const nonGameTermsPath = resolve(__dirname, '../../json/terms/nonGameTerms.json'); nonGameTermsCache = JSON.parse(readFileSync(nonGameTermsPath, 'utf8')); return nonGameTermsCache; diff --git a/lib/database.js b/lib/database/database.js similarity index 96% rename from lib/database.js rename to lib/database/database.js index 395ac39..ac12825 100644 --- a/lib/database.js +++ b/lib/database/database.js @@ -123,8 +123,11 @@ export async function initDB() { // 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) { diff --git a/lib/dboptimize.js b/lib/database/dboptimize.js similarity index 89% rename from lib/dboptimize.js rename to lib/database/dboptimize.js index 95a0b22..e685abc 100644 --- a/lib/dboptimize.js +++ b/lib/database/dboptimize.js @@ -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"); @@ -57,7 +57,7 @@ export async function optimizeDatabaseKws() { singleLineStatus( `Optimizing Keywords: ${i} / ${dbLength} ${((i / dbLength) * 100).toFixed( 2 - )}% (${proctime.elapsed()})` + )}% (${proctime.elapsed()}) Optimized Rows: ${changes}` ); for (let x = i; x < currentIndex + BATCH_SIZE; x++) { @@ -98,7 +98,7 @@ export async function optimizeDatabaseKws() { changed = true; } if (changed) { - result[promiseIndex].save(); + await result[promiseIndex].save(); changes++; } promiseIndex++; diff --git a/lib/models/file.js b/lib/database/models/file.js similarity index 100% rename from lib/models/file.js rename to lib/database/models/file.js diff --git a/lib/models/index.js b/lib/database/models/index.js similarity index 100% rename from lib/models/index.js rename to lib/database/models/index.js diff --git a/lib/models/metadata.js b/lib/database/models/metadata.js similarity index 100% rename from lib/models/metadata.js rename to lib/database/models/metadata.js diff --git a/lib/models/queryCount.js b/lib/database/models/queryCount.js similarity index 100% rename from lib/models/queryCount.js rename to lib/database/models/queryCount.js diff --git a/lib/dbkwworker.js b/lib/database/workers/dbkwworker.js similarity index 98% rename from lib/dbkwworker.js rename to lib/database/workers/dbkwworker.js index b1cb5dc..58c4e71 100644 --- a/lib/dbkwworker.js +++ b/lib/database/workers/dbkwworker.js @@ -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", diff --git a/lib/emulatorConfig.js b/lib/emulator/emulatorConfig.js similarity index 99% rename from lib/emulatorConfig.js rename to lib/emulator/emulatorConfig.js index cb3f9f3..fcf39b3 100644 --- a/lib/emulatorConfig.js +++ b/lib/emulator/emulatorConfig.js @@ -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 = { diff --git a/lib/consoleicons.js b/lib/images/consoleicons.js similarity index 100% rename from lib/consoleicons.js rename to lib/images/consoleicons.js diff --git a/lib/flag.js b/lib/images/flag.js similarity index 92% rename from lib/flag.js rename to lib/images/flag.js index 9ff6b55..1f11cad 100644 --- a/lib/flag.js +++ b/lib/images/flag.js @@ -3,7 +3,7 @@ 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"); +const flagsDir = path.join(__dirname, "../../views/public/images/flags"); export default class Flags { diff --git a/lib/emulators.json b/lib/json/dynamic_content/emulators.json similarity index 100% rename from lib/emulators.json rename to lib/json/dynamic_content/emulators.json diff --git a/lib/categories.json b/lib/json/terms/categories.json similarity index 100% rename from lib/categories.json rename to lib/json/terms/categories.json diff --git a/lib/nonGameTerms.json b/lib/json/terms/nonGameTerms.json similarity index 100% rename from lib/nonGameTerms.json rename to lib/json/terms/nonGameTerms.json diff --git a/lib/search.js b/lib/search/search.js similarity index 83% rename from lib/search.js rename to lib/search/search.js index e1dae80..ecae49f 100644 --- a/lib/search.js +++ b/lib/search/search.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, Metadata } from "./models/index.js"; +} from "../services/elasticsearch.js"; +import { File, Metadata } from "../database/models/index.js"; export default class Searcher { constructor(fields) { diff --git a/lib/services/elasticsearch.js b/lib/services/elasticsearch.js index aaa456a..bff5062 100644 --- a/lib/services/elasticsearch.js +++ b/lib/services/elasticsearch.js @@ -1,7 +1,7 @@ import { Client } from "@elastic/elasticsearch"; -import debugPrint from "../debugprint.js"; -import { File, Metadata } from "../models/index.js"; -import { Timer } from "../time.js"; +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", diff --git a/lib/asciiart.js b/lib/utility/asciiart.js similarity index 100% rename from lib/asciiart.js rename to lib/utility/asciiart.js diff --git a/lib/debugprint.js b/lib/utility/printutils.js similarity index 100% rename from lib/debugprint.js rename to lib/utility/printutils.js diff --git a/lib/taskqueue.js b/lib/utility/taskqueue.js similarity index 100% rename from lib/taskqueue.js rename to lib/utility/taskqueue.js diff --git a/lib/time.js b/lib/utility/time.js similarity index 100% rename from lib/time.js rename to lib/utility/time.js diff --git a/server.js b/server.js index 0f89045..2c1ba93 100644 --- a/server.js +++ b/server.js @@ -1,38 +1,38 @@ -import getAllFiles from "./lib/dircrawl.js"; -import { optimizeDatabaseKws } from "./lib/dboptimize.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, Metadata } 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 MetadataSearch from "./lib/metadatasearch.js"; -import Flag from "./lib/flag.js"; -import ConsoleIcons from "./lib/consoleicons.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 localeNamePath = "./lib/json/maps/name_localization.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 localeNames = await FileHandler.parseJsonFile(localeNamePath); let crawlTime = 0; let queryCount = 0; let fileCount = 0; @@ -40,7 +40,6 @@ let indexPage = "pages/index"; let flags = new Flag(); let consoleIcons = new ConsoleIcons(emulatorsData); - // Initialize databases await initDB(); await initElasticsearch(); @@ -73,11 +72,11 @@ for (let field in searchFields) { } let search = new Searcher(searchFields); -let metadataSearch = new MetadataSearch(); +let metadataManager = new MetadataManager(); async function getFilesJob() { console.log("Updating the file list."); - let oldFileCount = fileCount || 0 + let oldFileCount = fileCount || 0; fileCount = await getAllFiles(categoryList); if (!fileCount) { console.log("File update failed"); @@ -85,16 +84,16 @@ async function getFilesJob() { } crawlTime = Date.now(); console.log(`Finished updating file list. ${fileCount} found.`); - if(await Metadata.count() < await metadataSearch.getIGDBGamesCount()){ - await metadataSearch.syncAllMetadata(); + if ((await Metadata.count()) < (await metadataManager.getIGDBGamesCount())) { + await metadataManager.syncAllMetadata(); } - if(fileCount > oldFileCount){ - await metadataSearch.matchAllMetadata() + if (fileCount > oldFileCount) { + await metadataManager.matchAllMetadata(); } await optimizeDatabaseKws(); //this is less important and needs to run last. - if(fileCount > oldFileCount){ - metadataSearch.matchAllMetadata(true) + if (fileCount > oldFileCount) { + metadataManager.matchAllMetadata(true); } } @@ -220,7 +219,7 @@ app.get("/search", async function (req, res) { delete settings.combineWith; } let loadOldResults = - req.query.old === "true" || !await Metadata.count() ? true : false; + req.query.old === "true" || !(await Metadata.count()) ? true : false; settings.pageSize = loadOldResults ? 100 : 10; settings.page = pageNum - 1; settings.sort = req.query.o || ""; @@ -243,7 +242,7 @@ app.get("/search", async function (req, res) { settings: settings, flags: flags, consoleIcons: consoleIcons, - localeNames: localeNames + localeNames: localeNames, }; let page = loadOldResults ? "resultsold" : "results"; options = buildOptions(page, options); @@ -278,7 +277,7 @@ app.get("/lucky", async 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.oldSettingsAvailable = (await Metadata.count()) ? true : false; options = buildOptions(page, options); res.render(indexPage, options); }); @@ -333,10 +332,6 @@ app.get("/play/:id", async function (req, res) { app.get("/info/:id", async function (req, res) { //set header to allow video embed res.setHeader("Cross-Origin-Embedder-Policy", "unsafe-non"); - if (!metadataSearch.authorized) { - res.redirect("/"); - return; - } let romId = parseInt(req.params.id); let romFile = await search.findIndex(romId); if (!romFile) { @@ -345,14 +340,14 @@ app.get("/info/:id", async function (req, res) { } let options = { file: { - ...romFile.dataValues + ...romFile.dataValues, }, metadata: { - ...romFile?.details?.dataValues + ...romFile?.details?.dataValues, }, flags: flags, consoleIcons: consoleIcons, - localeNames: localeNames + localeNames: localeNames, }; let page = "info"; options = buildOptions(page, options); @@ -579,4 +574,3 @@ if ( } cron.schedule("0 30 2 * * *", getFilesJob); - From 45ae6e72c074a093e223da99950b49d1d0c43a16 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Sat, 31 May 2025 14:12:47 -0600 Subject: [PATCH 46/46] Finishing touches --- config/locales/ar.json | 1 + config/locales/bn.json | 9 +++++---- config/locales/de.json | 7 ++++--- config/locales/en.json | 1 + config/locales/es.json | 9 +++++---- config/locales/fr.json | 3 ++- config/locales/hi.json | 5 +++-- config/locales/it.json | 9 +++++---- config/locales/ja.json | 9 +++++---- config/locales/ko.json | 9 +++++---- config/locales/pl.json | 3 ++- config/locales/pt.json | 7 ++++--- config/locales/romaji.json | 9 +++++---- config/locales/ru.json | 3 ++- config/locales/tr.json | 9 +++++---- config/locales/zh.json | 5 +++-- server.js | 14 ++++++++++++++ views/partials/footer.ejs | 3 +++ 18 files changed, 74 insertions(+), 41 deletions(-) diff --git a/config/locales/ar.json b/config/locales/ar.json index 18ffee4..e6e1c5a 100644 --- a/config/locales/ar.json +++ b/config/locales/ar.json @@ -103,6 +103,7 @@ "queries": "عدد الاستعلامات:", "files": "الملفات المعروفة:", "terms": "عدد المصطلحات:", + "metadata": "الملفات مع البيانات الوصفية:", "last_crawl": "وقت آخر زحف:" }, "emulator": { diff --git a/config/locales/bn.json b/config/locales/bn.json index e7725a6..ba9b3d4 100644 --- a/config/locales/bn.json +++ b/config/locales/bn.json @@ -100,10 +100,11 @@ "save": "সেটিংস সংরক্ষণ করুন" }, "footer": { - "queries": "অনুসন্ধান সংখ্যা:", - "files": "পরিচিত ফাইল:", - "terms": "শব্দ সংখ্যা:", - "last_crawl": "সর্বশেষ আপডেটের সময়:" + "queries": "কোয়েরির সংখ্যা:", + "files": "জানা ফাইল:", + "terms": "শব্দের সংখ্যা:", + "metadata": "মেটাডেটা সহ ফাইল:", + "last_crawl": "সর্বশেষ ক্রল সময়:" }, "emulator": { "loading": { diff --git a/config/locales/de.json b/config/locales/de.json index 69f9d77..ae5ebaf 100644 --- a/config/locales/de.json +++ b/config/locales/de.json @@ -100,10 +100,11 @@ "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": { diff --git a/config/locales/en.json b/config/locales/en.json index 22f1fc5..a0f3b8e 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -103,6 +103,7 @@ "queries": "Number of Queries:", "files": "Known Files:", "terms": "Term Count:", + "metadata": "Files with Metadata:", "last_crawl": "Time of Last Crawl:" }, "emulator": { diff --git a/config/locales/es.json b/config/locales/es.json index 9adb77c..7c69eef 100644 --- a/config/locales/es.json +++ b/config/locales/es.json @@ -100,10 +100,11 @@ "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": { diff --git a/config/locales/fr.json b/config/locales/fr.json index 15d5a86..e33de32 100644 --- a/config/locales/fr.json +++ b/config/locales/fr.json @@ -103,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": { diff --git a/config/locales/hi.json b/config/locales/hi.json index 3b30360..d6b4b03 100644 --- a/config/locales/hi.json +++ b/config/locales/hi.json @@ -101,9 +101,10 @@ }, "footer": { "queries": "क्वेरी की संख्या:", - "files": "ज्ञात फाइलें:", + "files": "ज्ञात फ़ाइलें:", "terms": "शब्दों की संख्या:", - "last_crawl": "अंतिम अपडेट का समय:" + "metadata": "मेटाडेटा वाली फ़ाइलें:", + "last_crawl": "अंतिम क्रॉल का समय:" }, "emulator": { "loading": { diff --git a/config/locales/it.json b/config/locales/it.json index 6c72755..be1c958 100644 --- a/config/locales/it.json +++ b/config/locales/it.json @@ -100,10 +100,11 @@ "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": { diff --git a/config/locales/ja.json b/config/locales/ja.json index 9634901..efd701d 100644 --- a/config/locales/ja.json +++ b/config/locales/ja.json @@ -100,10 +100,11 @@ "save": "設定を保存" }, "footer": { - "queries": "検索クエリ数:", - "files": "登録ファイル数:", - "terms": "インデックス語数:", - "last_crawl": "最終更新時刻:" + "queries": "クエリ数:", + "files": "既知のファイル:", + "terms": "用語数:", + "metadata": "メタデータ付きファイル:", + "last_crawl": "最終クロール時刻:" }, "emulator": { "loading": { diff --git a/config/locales/ko.json b/config/locales/ko.json index fdae9aa..b3fa53d 100644 --- a/config/locales/ko.json +++ b/config/locales/ko.json @@ -100,10 +100,11 @@ "save": "설정 저장" }, "footer": { - "queries": "검색 횟수:", - "files": "등록된 파일:", - "terms": "색인된 단어 수:", - "last_crawl": "마지막 업데이트:" + "queries": "쿼리 수:", + "files": "알려진 파일:", + "terms": "용어 수:", + "metadata": "메타데이터가 있는 파일:", + "last_crawl": "마지막 크롤링 시간:" }, "emulator": { "loading": { diff --git a/config/locales/pl.json b/config/locales/pl.json index 283686a..9ef8d2e 100644 --- a/config/locales/pl.json +++ b/config/locales/pl.json @@ -103,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": { diff --git a/config/locales/pt.json b/config/locales/pt.json index 76ce834..c13d25e 100644 --- a/config/locales/pt.json +++ b/config/locales/pt.json @@ -100,10 +100,11 @@ "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": { diff --git a/config/locales/romaji.json b/config/locales/romaji.json index f9485d7..1c412a1 100644 --- a/config/locales/romaji.json +++ b/config/locales/romaji.json @@ -100,10 +100,11 @@ "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": { diff --git a/config/locales/ru.json b/config/locales/ru.json index fb2a8b1..5b9019c 100644 --- a/config/locales/ru.json +++ b/config/locales/ru.json @@ -103,7 +103,8 @@ "queries": "Количество запросов:", "files": "Известные файлы:", "terms": "Количество терминов:", - "last_crawl": "Время последнего обхода:" + "metadata": "Файлы с Метаданными:", + "last_crawl": "Время последнего сканирования:" }, "emulator": { "loading": { diff --git a/config/locales/tr.json b/config/locales/tr.json index dc548fa..eb65f27 100644 --- a/config/locales/tr.json +++ b/config/locales/tr.json @@ -100,10 +100,11 @@ "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": { diff --git a/config/locales/zh.json b/config/locales/zh.json index 8eef551..a7eac2b 100644 --- a/config/locales/zh.json +++ b/config/locales/zh.json @@ -102,8 +102,9 @@ "footer": { "queries": "查询次数:", "files": "已知文件:", - "terms": "索引词数:", - "last_crawl": "最近更新时间:" + "terms": "术语数量:", + "metadata": "带元数据的文件:", + "last_crawl": "最后爬取时间:" }, "emulator": { "loading": { diff --git a/server.js b/server.js index 2c1ba93..b9beb7a 100644 --- a/server.js +++ b/server.js @@ -36,9 +36,11 @@ 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(); @@ -48,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"]; @@ -86,6 +91,9 @@ async function getFilesJob() { 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(); @@ -95,6 +103,9 @@ async function getFilesJob() { if (fileCount > oldFileCount) { metadataManager.matchAllMetadata(true); } + metadataMatchCount = await File.count({ + where: { detailsId: { [Op.ne]: null } }, + }); } function buildOptions(page, options) { @@ -105,6 +116,7 @@ let defaultOptions = { crawlTime: crawlTime, queryCount: queryCount, fileCount: fileCount, + metadataMatchCount: metadataMatchCount, generateAsciiArt: generateAsciiArt, isEmulatorCompatible: isEmulatorCompatible, isNonGameContent: isNonGameContent, @@ -115,6 +127,7 @@ function updateDefaults() { defaultOptions.crawlTime = crawlTime; defaultOptions.queryCount = queryCount; defaultOptions.fileCount = fileCount; + defaultOptions.metadataMatchCount = metadataMatchCount; } let app = express(); @@ -563,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 ( diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs index 4d7a7ce..91f2f95 100644 --- a/views/partials/footer.ejs +++ b/views/partials/footer.ejs @@ -4,6 +4,8 @@
                |
                <%= __('footer.files') %>
                |
                +
                <%= __('footer.metadata') %>
                +
                |
                <%= __('footer.last_crawl') %>
                @@ -11,5 +13,6 @@
                <%= __('results.table.name') %><%= __('results.table.group') %><%= __('results.table.category') %><%= __('results.table.region') %><%= __('results.table.type') %><%= __('results.table.size') %><%= __('results.table.date') %><%= __('results.table.score') %><%= __('results.table.name') %><%= __('results.table.group') %><%= __('results.table.category') %><%= __('results.table.region') %><%= __('results.table.type') %><%= __('results.table.size') %><%= __('results.table.date') %><%= __('results.table.score') %><%= __('results.table.play') %>
                - - <%= results.items[x].filename %> + + <%= results[x].file.filename %> - <%= results.items[x].group %> + <%= results[x].file.group %> - <%= results.items[x].category %> + <%= results[x].file.category %> - <%= results.items[x].region %> + <%= results[x].file.region %> - <%= results.items[x].type %> + <%= results[x].file.type %> - <%= results.items[x].size %> + <%= results[x].file.size %> - <%= results.items[x].date %> + <%= results[x].file.date %> - <%= results.items[x].score.toFixed(2) %> + <%= results[x].score.toFixed(2) %> - <% if (isEmulatorCompatible(results.items[x].category)) { %> - <%= __('emulator.play') %> + <% if (isEmulatorCompatible(results[x].category)) { %> + <%= __('emulator.play') %> <% } else { %> <% } %> @@ -137,7 +134,9 @@
              • class="page-link previous" aria-controls="results" aria-disabled="true" aria-label="Previous" data-dt-idx="previous" tabindex="-1">‹
              • 1
              • <%- 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; + %>
              • <%= x %>
              • <% } %> <%- pageNum <= pageCount - 5 ? ellipsesElem : '' %> @@ -152,31 +151,28 @@ \ No newline at end of file diff --git a/views/pages/settings.ejs b/views/pages/settings.ejs index 24aa745..17959cf 100644 --- a/views/pages/settings.ejs +++ b/views/pages/settings.ejs @@ -48,11 +48,15 @@ +
                + diff --git a/views/partials/carousel.ejs b/views/partials/carousel.ejs new file mode 100644 index 0000000..1f34143 --- /dev/null +++ b/views/partials/carousel.ejs @@ -0,0 +1,45 @@ +<% + if(typeof videos === 'undefined'){ + let videos = [] + } + if(typeof images === 'undefined'){ + let images = [] + } +%> + + \ No newline at end of file diff --git a/views/partials/result.ejs b/views/partials/result.ejs index 77a5677..db7ae44 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -9,21 +9,31 @@

                <%= metadata.title || file.filename %>

                -

                Released: <%= metadata.releasedate || file.date %> - Region: <%= file.region %> - Platform: <%= file.category %> +

                <%= __('search.released') %>: <%= metadata.releasedate || file.date %> + <%= __('search.region') %>: <%= file.region %> + <%= __('search.platform') %>: <%= file.category %> <% if(metadata.genre){ %> Genres: <%= JSON.parse(metadata.genre).join(' / ') %> <% } %>

                -

                <%= metadata.description || "No description was found."//todo: localize %>

                + <% if(metadata.title) {%> +

                <%= metadata.description || __('search.no_description') %>

                + <% } else { %> +

                <%= __('search.no_metadata') %>

                + <% } %> <% if(metadata.title) {%>

                Filename: <%= file.filename %>

                <% } %>

                Release Group: <%= file.group %>

                - More Info - Download - Play In Browser + More Info + Download + <% if (process.env.EMULATOR_ENABLED === 'true') { %> + <% if (isEmulatorCompatible(file.category)) { %> + <%= __('emulator.play')%> <% } else { %> + + <% } + }%> +

                \ No newline at end of file diff --git a/views/public/css/info.css b/views/public/css/info.css new file mode 100644 index 0000000..1cc0c99 --- /dev/null +++ b/views/public/css/info.css @@ -0,0 +1,46 @@ +.info { + font-weight: bold; + color: #f0a400; +} +.stars { + color: #f0a400; +} + +.embed-responsive iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.embed-responsive { + position: relative; + 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; +} \ No newline at end of file diff --git a/views/public/css/resultsold.css b/views/public/css/resultsold.css new file mode 100644 index 0000000..592a9dd --- /dev/null +++ b/views/public/css/resultsold.css @@ -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; +} diff --git a/views/public/css/style.css b/views/public/css/style.css index 81167f4..136009f 100644 --- a/views/public/css/style.css +++ b/views/public/css/style.css @@ -144,43 +144,3 @@ td a { position: relative; z-index: 9999 !important; } -.dt-orderable { - position: absolute; - - -} -.dt-column-order { - position: absolute; - width: 12px; - opacity: 0.125; - 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-desc::before{ - opacity: 1; -} -thead > tr > th.dt-orderable span.dt-column-order.order-asc::after{ - opacity: 1; -} diff --git a/views/public/js/settings.js b/views/public/js/settings.js index d1514d7..61ca2b8 100644 --- a/views/public/js/settings.js +++ b/views/public/js/settings.js @@ -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,6 +52,7 @@ 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 = '/' } diff --git a/views/public/js/video.js b/views/public/js/video.js new file mode 100644 index 0000000..9b5371a --- /dev/null +++ b/views/public/js/video.js @@ -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, + }, + }); +} \ No newline at end of file From 60727a09e1426c5a0180fb8fe1c27b9827930e04 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Tue, 27 May 2025 08:20:19 -0600 Subject: [PATCH 20/46] raise limit of all json storage fields --- lib/models/metadata.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/models/metadata.js b/lib/models/metadata.js index cfeaded..5fb7d93 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -14,7 +14,7 @@ export default function (sequelize) { type: DataTypes.STRING }, description: { - type: DataTypes.TEXT + type: DataTypes.STRING(2048) }, rating: { type: DataTypes.STRING @@ -25,26 +25,27 @@ export default function (sequelize) { releasedate: { type: DataTypes.DATEONLY }, + //anything that stores as json make the limit much higher genre: { - type: DataTypes.STRING + type: DataTypes.STRING(2048) }, developers: { - type: DataTypes.STRING + type: DataTypes.STRING(2048) }, publishers: { - type: DataTypes.STRING + type: DataTypes.STRING(2048) }, gamemodes:{ - type: DataTypes.STRING + type: DataTypes.STRING(2048) }, platforms: { - type: DataTypes.STRING + type: DataTypes.STRING(2048) }, screenshots: { - type: DataTypes.STRING + type: DataTypes.STRING(2048) }, videos:{ - type: DataTypes.STRING + type: DataTypes.STRING(2048) } }, { indexes: [ From c2e087331ad7520a9216865a0fda216a97278181 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Tue, 27 May 2025 08:54:26 -0600 Subject: [PATCH 21/46] update envs --- .env | 2 +- docker-compose.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.env b/.env index 5519058..0e50335 100644 --- a/.env +++ b/.env @@ -25,6 +25,6 @@ POSTGRES_PASSWORD=development # Elasticsearch Configuration ELASTICSEARCH_URL=http://localhost:9200 -#IGDB Connection Configuration +#IGDB Connection Configuration - Not setting this will disable the new search page and metadata pull TWITCH_CLIENT_ID= TWITCH_CLIENT_SECRET= \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 74d98ea..60f7e27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,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: From cf70ee7ce00b8438150491e47d715a94af3663bd Mon Sep 17 00:00:00 2001 From: Alexandra Date: Tue, 27 May 2025 17:32:25 -0600 Subject: [PATCH 22/46] update translations --- config/i18n.js | 1 + config/locales/ar.json | 29 +++- config/locales/bn.json | 27 +++- config/locales/de.json | 307 +++++++++++++++++++---------------- config/locales/en.json | 320 +++++++++++++++++++------------------ config/locales/es.json | 31 +++- config/locales/fr.json | 21 ++- config/locales/hi.json | 26 ++- config/locales/it.json | 29 +++- config/locales/ja.json | 21 ++- config/locales/ko.json | 22 ++- config/locales/pl.json | 28 +++- config/locales/pt.json | 21 ++- config/locales/romaji.json | 22 ++- config/locales/ru.json | 28 +++- config/locales/tr.json | 29 +++- config/locales/zh.json | 21 ++- views/pages/info.ejs | 20 ++- views/pages/results.ejs | 102 ++++++------ views/partials/result.ejs | 14 +- 20 files changed, 726 insertions(+), 393 deletions(-) diff --git a/config/i18n.js b/config/i18n.js index 6ed6cdf..818225d 100644 --- a/config/i18n.js +++ b/config/i18n.js @@ -24,6 +24,7 @@ console.log('Available locales:', availableLocales); i18n.configure({ locales: availableLocales, defaultLocale: 'en', + retryInDefaultLocale: true, directory: localesDir, objectNotation: true, updateFiles: false, diff --git a/config/locales/ar.json b/config/locales/ar.json index c0446a2..b71ccba 100644 --- a/config/locales/ar.json +++ b/config/locales/ar.json @@ -21,7 +21,22 @@ "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": "المزيد من المعلومات" }, "about": { "title": "حول الموقع", @@ -40,6 +55,10 @@ "credits": { "created_by": "تم إنشاء محرك البحث بواسطة", "view_github": "عرض المشروع على GitHub" + }, + "metadata": { + "title": "معلومات البيانات الوصفية", + "description": "يقوم هذا الموقع بجلب معلومات البيانات الوصفية عن الألعاب من {{metadata_source}}. يتم جلب هذه المعلومات عند البحث. قد تكون عمليات البحث بطيئة حتى يتم تخزين البيانات الوصفية في قاعدة البيانات." } }, "settings": { @@ -67,8 +86,12 @@ "tooltip": "يتطلب تطابق جميع الكلمات في استعلام البحث." }, "hide_non_game": { - "label": "إخفاء المحتوى غير اللعبي", - "tooltip": "يفلتر تعديلات الـ ROM، والتصحيحات، والرسومات، والمحتويات الأخرى غير اللعبية من نتائج البحث." + "label": "إخفاء المحتوى غير المتعلق بالألعاب", + "tooltip": "يقوم بتصفية تعديلات ROM والتصحيحات والرسومات والمحتويات الأخرى غير المتعلقة بالألعاب من نتائج البحث." + }, + "use_old_results": { + "label": "البحث القديم", + "tooltip": "يعيد توجيه بحثك إلى نتائج التنسيق الجدولي القديم. ستحمل هذه النتائج بشكل أسرع لأنها ستتجاهل سحب البيانات الوصفية." } }, "save": "حفظ الإعدادات" diff --git a/config/locales/bn.json b/config/locales/bn.json index 7368d2b..f23a564 100644 --- a/config/locales/bn.json +++ b/config/locales/bn.json @@ -21,7 +21,22 @@ "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": "আরও তথ্য" }, "about": { "title": "সম্পর্কে", @@ -40,6 +55,10 @@ "credits": { "created_by": "সার্চ ইঞ্জিন তৈরি করেছেন", "view_github": "GitHub-এ প্রকল্প দেখুন" + }, + "metadata": { + "title": "মেটাডেটা তথ্য", + "description": "এই ওয়েবসাইটটি {{metadata_source}} থেকে গেমের মেটাডেটা তথ্য সংগ্রহ করে। এই তথ্য অনুসন্ধানের সময় সংগ্রহ করা হয়। মেটাডেটা ডাটাবেসে ক্যাশ না হওয়া পর্যন্ত অনুসন্ধান কুয়েরি ধীর হতে পারে।" } }, "settings": { @@ -68,7 +87,11 @@ }, "hide_non_game": { "label": "নন-গেম কন্টেন্ট লুকান", - "tooltip": "ROM হ্যাকস, প্যাচ, আর্টওয়ার্ক এবং অন্যান্য নন-গেম কন্টেন্ট অনুসন্ধান ফলাফল থেকে ফিল্টার করে।" + "tooltip": "সার্চ রেজাল্ট থেকে ROM হ্যাকস, প্যাচ, আর্টওয়ার্ক এবং অন্যান্য নন-গেম কন্টেন্ট ফিল্টার করে।" + }, + "use_old_results": { + "label": "পুরানো সার্চ", + "tooltip": "আপনার সার্চকে পুরানো টেবিল ফরম্যাটের রেজাল্টে রিডাইরেক্ট করে। এগুলি মেটাডেটা পুল করা এড়িয়ে যাবে তাই দ্রুত লোড হবে।" } }, "save": "সেটিংস সংরক্ষণ করুন" diff --git a/config/locales/de.json b/config/locales/de.json index d1f3d52..cf7eed1 100644 --- a/config/locales/de.json +++ b/config/locales/de.json @@ -1,149 +1,172 @@ { - "app": { - "name": "Myrient Search", - "description": "Eine Suchmaschine für Myrient - ein von Erista entwickelter Dienst zur Bewahrung von Videospielen.", - "tagline": "Myrient bewahrt Videospielsammlungen in einer organisierten und öffentlich zugänglichen Form, damit sie nicht in Vergessenheit geraten.", - "disclaimer": "Nicht mit Myrient/Erista verbunden!" - }, - "nav": { - "settings": "Einstellungen", - "emulators": "Emulatoren", - "about": "Über uns", - "search": "Suche", - "results": "Ergebnisse" - }, - "search": { - "placeholder": "Suchen...", - "button": "Suchen", - "lucky": "Ich habe Glück", - "found": "{{count}} Ergebnis gefunden", - "found_plural": "{{count}} Ergebnisse gefunden", - "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." - }, - "about": { - "title": "Über uns", - "support": "Wenn Ihnen dieses Projekt gefällt, könnten Sie Myrient unterstützen:", - "donate": "An Myrient spenden", - "emulator": { - "title": "Integrierter Emulator", - "description": "Diese Website enthält einen integrierten Emulator, der mit EmulatorJS betrieben wird und Retro-Spiele direkt in Ihrem Browser spielbar macht.", - "compatibility": "Kompatible Spiele haben einen Play-Button auf ihrer Ergebnisseite.", - "browser_tip": "Für die beste Spielerfahrung empfehlen wir einen Chromium-basierten Browser mit aktivierter Hardwarebeschleunigung.", - "save_states": "Spiele werden direkt aus dem öffentlichen Archiv von Myrient geladen. Spielstände werden lokal in Ihrem Browser gespeichert.", - "limitations": "ROM-Hacks, Soundtracks und andere Nicht-Spielinhalte werden vom Emulator nicht unterstützt und lassen sich möglicherweise nicht laden.", - "disabled": "Die Webemulator-Funktion wurde vom Administrator deaktiviert.", - "contact": "Kontaktieren Sie den Administrator oder starten Sie Ihre eigene Instanz von Myrient Search." - }, - "credits": { - "created_by": "Suchmaschine entwickelt von", - "view_github": "Projekt auf GitHub ansehen" - } - }, - "settings": { - "title": "Einstellungen", - "search_columns": { - "title": "Suchspalten", - "tooltip": "Legt fest, in welchen Spalten die Suchmaschine suchen soll." - }, - "score_multiplier": { - "title": "Relevanz-Multiplikator", - "tooltip": "Passt die Gewichtung für Treffer je nach Kategorie an, in der ein Suchwort gefunden wurde." - }, - "extras": { - "title": "Erweiterte Einstellungen", - "fuzzy": { - "label": "Unscharfe Suche", - "tooltip": "Wert zwischen 0,00 und 1,00, der bestimmt, wie ähnlich ein Wort sein muss, um als Treffer zu gelten (Levenshtein-Distanz). Höhere Werte erlauben ungenauere Treffer. Der Wert 0 deaktiviert diese Funktion." - }, - "prefix": { - "label": "Wortanfänge berücksichtigen", - "tooltip": "Erlaubt Treffer, wenn nur der Anfang eines Wortes übereinstimmt." - }, - "match_all": { - "label": "Alle Wörter abgleichen", - "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." - } - }, - "save": "Einstellungen speichern" - }, - "footer": { - "queries": "Anzahl der Suchanfragen:", - "files": "Bekannte Dateien:", - "terms": "Indexierte Begriffe:", - "last_crawl": "Letztes Update:" - }, + "app": { + "name": "Myrient Search", + "description": "Eine Suchmaschine für Myrient - ein von Erista entwickelter Dienst zur Bewahrung von Videospielen.", + "tagline": "Myrient bewahrt Videospielsammlungen in einer organisierten und öffentlich zugänglichen Form, damit sie nicht in Vergessenheit geraten.", + "disclaimer": "Nicht mit Myrient/Erista verbunden!" + }, + "nav": { + "settings": "Einstellungen", + "emulators": "Emulatoren", + "about": "Über uns", + "search": "Suche", + "results": "Ergebnisse" + }, + "search": { + "placeholder": "Suchen...", + "button": "Suchen", + "lucky": "Ich habe Glück", + "found": "{{count}} Ergebnis gefunden", + "found_plural": "{{count}} Ergebnisse gefunden", + "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.", + "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" + }, + "about": { + "title": "Über uns", + "support": "Wenn Ihnen dieses Projekt gefällt, könnten Sie Myrient unterstützen:", + "donate": "An Myrient spenden", "emulator": { - "loading": { - "rom": "ROM wird geladen...", - "downloading": "ROM wird heruntergeladen", - "downloading_progress": "ROM-Download: {{percent}}%", - "decompressing": "Spiel wird entpackt..." - }, - "error": { - "loading": "Fehler beim Laden des Spiels", - "no_rom": "Keine ROM-Datei im Archiv gefunden", - "http_error": "HTTP-Fehler! Status: {{status}}" - }, - "warning": { - "non_game": "Warnung: Diese Datei ist möglicherweise keine Spiel-ROM und funktioniert möglicherweise nicht richtig im Web-Emulator.", - "see_about": "Weitere Informationen finden Sie auf der {{link}}-Seite.", - "no_data": "Keine Emulator-Daten verfügbar.", - "https": "Unsichere HTTP-Verbindung: Einige Emulatoren benötigen HTTPS, um richtig zu funktionieren. Diese Seite ist nicht korrekt eingerichtet." - }, - "console": { - "about": "Dies ist ein Online-Emulator, der Spiele direkt aus dem öffentlichen Archiv von Myrient lädt.", - "disclaimer": "Auf dieser Website werden keine Spiele-ROMs gehostet. Alle Inhalte werden direkt von den Myrient-Servern geladen.", - "more_info": "Weitere Informationen zu diesem Dienst finden Sie auf der Über uns-Seite." - }, - "recommended": "Empfohlene Emulatoren", - "download": "Herunterladen", - "play": "Spielen", - "not_available": "----", - "disclaimer": "Dieser Emulator lädt Spiele direkt von {{link}}. Mehr dazu auf der {{about}}-Seite." + "title": "Integrierter Emulator", + "description": "Diese Website enthält einen integrierten Emulator, der mit EmulatorJS betrieben wird und Retro-Spiele direkt in Ihrem Browser spielbar macht.", + "compatibility": "Kompatible Spiele haben einen Play-Button auf ihrer Ergebnisseite.", + "browser_tip": "Für die beste Spielerfahrung empfehlen wir einen Chromium-basierten Browser mit aktivierter Hardwarebeschleunigung.", + "save_states": "Spiele werden direkt aus dem öffentlichen Archiv von Myrient geladen. Spielstände werden lokal in Ihrem Browser gespeichert.", + "limitations": "ROM-Hacks, Soundtracks und andere Nicht-Spielinhalte werden vom Emulator nicht unterstützt und lassen sich möglicherweise nicht laden.", + "disabled": "Die Webemulator-Funktion wurde vom Administrator deaktiviert.", + "contact": "Kontaktieren Sie den Administrator oder starten Sie Ihre eigene Instanz von Myrient Search." }, - "results": { - "table": { - "name": "Name", - "group": "Gruppe", - "category": "Kategorie", - "region": "Region", - "type": "Typ", - "size": "Größe", - "date": "Datum", - "score": "Relevanz", - "play": "Spielen" - } + "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}}. Diese Informationen werden bei der Suche abgerufen. Suchanfragen können langsam sein, bis die Metadaten in der Datenbank zwischengespeichert sind." + } + }, + "settings": { + "title": "Einstellungen", + "search_columns": { + "title": "Suchspalten", + "tooltip": "Legt fest, in welchen Spalten die Suchmaschine suchen soll." + }, + "score_multiplier": { + "title": "Relevanz-Multiplikator", + "tooltip": "Passt die Gewichtung für Treffer je nach Kategorie an, in der ein Suchwort gefunden wurde." + }, + "extras": { + "title": "Erweiterte Einstellungen", + "fuzzy": { + "label": "Unscharfe Suche", + "tooltip": "Wert zwischen 0,00 und 1,00, der bestimmt, wie ähnlich ein Wort sein muss, um als Treffer zu gelten (Levenshtein-Distanz). Höhere Werte erlauben ungenauere Treffer. Der Wert 0 deaktiviert diese Funktion." + }, + "prefix": { + "label": "Wortanfänge berücksichtigen", + "tooltip": "Erlaubt Treffer, wenn nur der Anfang eines Wortes übereinstimmt." + }, + "match_all": { + "label": "Alle Wörter abgleichen", + "tooltip": "Erfordert, dass jedes Wort der Suchanfrage gefunden wird." + }, + "hide_non_game": { + "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:", + "files": "Bekannte Dateien:", + "terms": "Indexierte Begriffe:", + "last_crawl": "Letztes Update:" + }, + "emulator": { + "loading": { + "rom": "ROM wird geladen...", + "downloading": "ROM wird heruntergeladen", + "downloading_progress": "ROM-Download: {{percent}}%", + "decompressing": "Spiel wird entpackt..." }, "error": { - "title": "Fehler", - "message": "Etwas ist schiefgelaufen", - "details": "Fehlerdetails: {{message}}", - "back_home": "Zurück zur Startseite", - "go_back": "Zurück" + "loading": "Fehler beim Laden des Spiels", + "no_rom": "Keine ROM-Datei im Archiv gefunden", + "http_error": "HTTP-Fehler! Status: {{status}}" }, - "languages": { - "en": "English", - "es": "Español", - "pt": "Português", - "zh": "中文", - "ja": "日本語", - "fr": "Français", - "de": "Deutsch", - "ko": "한국어", - "pl": "Polski", - "tr": "Türkçe", - "it": "Italiano", - "romaji": "Romaji", - "hi": "हिन्दी", - "ar": "العربية", - "bn": "বাংলা", - "ru": "Русский" + "warning": { + "non_game": "Warnung: Diese Datei ist möglicherweise keine Spiel-ROM und funktioniert möglicherweise nicht richtig im Web-Emulator.", + "see_about": "Weitere Informationen finden Sie auf der {{link}}-Seite.", + "no_data": "Keine Emulator-Daten verfügbar.", + "https": "Unsichere HTTP-Verbindung: Einige Emulatoren benötigen HTTPS, um richtig zu funktionieren. Diese Seite ist nicht korrekt eingerichtet." + }, + "console": { + "about": "Dies ist ein Online-Emulator, der Spiele direkt aus dem öffentlichen Archiv von Myrient lädt.", + "disclaimer": "Auf dieser Website werden keine Spiele-ROMs gehostet. Alle Inhalte werden direkt von den Myrient-Servern geladen.", + "more_info": "Weitere Informationen zu diesem Dienst finden Sie auf der Über uns-Seite." + }, + "recommended": "Empfohlene Emulatoren", + "download": "Herunterladen", + "play": "Spielen", + "not_available": "----", + "disclaimer": "Dieser Emulator lädt Spiele direkt von {{link}}. Mehr dazu auf der {{about}}-Seite." + }, + "results": { + "table": { + "name": "Name", + "group": "Gruppe", + "category": "Kategorie", + "region": "Region", + "type": "Typ", + "size": "Größe", + "date": "Datum", + "score": "Relevanz", + "play": "Spielen" } -} \ No newline at end of file + }, + "error": { + "title": "Fehler", + "message": "Etwas ist schiefgelaufen", + "details": "Fehlerdetails: {{message}}", + "back_home": "Zurück zur Startseite", + "go_back": "Zurück" + }, + "languages": { + "en": "English", + "es": "Español", + "pt": "Português", + "zh": "中文", + "ja": "日本語", + "fr": "Français", + "de": "Deutsch", + "ko": "한국어", + "pl": "Polski", + "tr": "Türkçe", + "it": "Italiano", + "romaji": "Romaji", + "hi": "हिन्दी", + "ar": "العربية", + "bn": "বাংলা", + "ru": "Русский" + } +} diff --git a/config/locales/en.json b/config/locales/en.json index d878da2..82dd372 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -1,162 +1,172 @@ { - "app": { - "name": "Myrient Search", - "description": "A search engine for Myrient - a service by Erista dedicated to video game preservation.", - "tagline": "Myrient offers organized and publicly available video game collections, keeping them from becoming lost to time.", - "disclaimer": "Not affiliated with Myrient/Erista!" - }, - "nav": { - "settings": "Settings", - "emulators": "Emulators", - "about": "About", - "search": "Search", - "results": "Results" - }, - "search": { - "placeholder": "Search...", - "button": "Search", - "lucky": "I'm Feeling Lucky", - "found": "Found {{count}} result", - "found_plural": "Found {{count}} results", - "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", - "no_description": "No descrption found.", - "no_metadata": "No metadata found.", - "displaying_results": "Displaying results {{start}} through {{end}}.", - "released": "Released", - "region": "Region", - "platform": "Platform" - }, - "about": { - "title": "About", - "support": "If you like this project, please consider supporting Myrient:", - "donate": "Donate to Myrient", - "emulator": { - "title": "Built-in Emulator", - "description": "This website includes a built-in emulator powered by EmulatorJS that brings retro gaming directly to your browser.", - "compatibility": "Compatible games will feature a play button on their search result page.", - "browser_tip": "For the best gaming experience, use a Chromium-based browser with hardware acceleration turned on.", - "save_states": "Games are loaded directly from Myrient's public archive. Save states are stored locally in the browser.", - "limitations": "ROM hacks, soundtracks, and other non-game content are not supported by the emulator and may fail to load.", - "disabled": "Web Emulator functionality was disabled by the administrator.", - "contact": "Contact the administrator or spin up your own instance of Myrient Search." - }, - "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}}. This information is pulled on search. Search queries may be slow until metadata has been cached in the database." - } - }, - "settings": { - "title": "Settings", - "search_columns": { - "title": "Search Columns", - "tooltip": "Selects which columns the search engine will search on." - }, - "score_multiplier": { - "title": "Search Score Multiplier", - "tooltip": "Multiplies the match score for each word found based on the category it's found in." - }, - "extras": { - "title": "Extras", - "fuzzy": { - "label": "Fuzzy Value", - "tooltip": "Value between 0.00 and 1.00 that determines the fuzzy distance (Levenshtein distance) for how closely a word needs to be considered a match. A higher value allows for less stringent matches. A value of 0 disables." - }, - "prefix": { - "label": "Allow Prefixes", - "tooltip": "Allows partial matches of words at the start of the word." - }, - "match_all": { - "label": "Match All Words", - "tooltip": "Requires all words in the search query to match." - }, - "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": "Redirects your search to the old table formatted results. These will load faster as they will ignore pulling metadata." - } - }, - "save": "Save Settings" - }, - "footer": { - "queries": "Number of Queries:", - "files": "Known Files:", - "terms": "Term Count:", - "last_crawl": "Time of Last Crawl:" - }, + "app": { + "name": "Myrient Search", + "description": "A search engine for Myrient - a service by Erista dedicated to video game preservation.", + "tagline": "Myrient offers organized and publicly available video game collections, keeping them from becoming lost to time.", + "disclaimer": "Not affiliated with Myrient/Erista!" + }, + "nav": { + "settings": "Settings", + "emulators": "Emulators", + "about": "About", + "search": "Search", + "results": "Results" + }, + "search": { + "placeholder": "Search...", + "button": "Search", + "lucky": "I'm Feeling Lucky", + "found": "Found {{count}} result", + "found_plural": "Found {{count}} results", + "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", + "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" + }, + "about": { + "title": "About", + "support": "If you like this project, please consider supporting Myrient:", + "donate": "Donate to Myrient", "emulator": { - "loading": { - "rom": "Loading ROM...", - "downloading": "Downloading ROM", - "downloading_progress": "Downloading ROM: {{percent}}%", - "decompressing": "Decompressing the game..." - }, - "error": { - "loading": "Error loading game", - "no_rom": "No ROM file found in archive", - "http_error": "HTTP error! status: {{status}}" - }, - "warning": { - "non_game": "Warning: This file may not be a game ROM and might not work properly in the web emulator.", - "see_about": "See the {{link}} page for more information.", - "no_data": "No emulator data available.", - "https": "Insecure HTTP: Some emulators require HTTPS to function properly. This instance isn't properly configured." - }, - "console": { - "about": "This is an online emulator running game ROMs directly from Myrient's public archive.", - "disclaimer": "No game ROMs are hosted on this website. All content is loaded directly from Myrient servers.", - "more_info": "For more information about this service, please visit the About page." - }, - "recommended": "Recommended Emulators", - "download": "Download", - "play": "Play", - "not_available": "----", - "disclaimer": "This emulator loads games directly from {{link}}. Learn more on the {{about}} page." + "title": "Built-in Emulator", + "description": "This website includes a built-in emulator powered by EmulatorJS that brings retro gaming directly to your browser.", + "compatibility": "Compatible games will feature a play button on their search result page.", + "browser_tip": "For the best gaming experience, use a Chromium-based browser with hardware acceleration turned on.", + "save_states": "Games are loaded directly from Myrient's public archive. Save states are stored locally in the browser.", + "limitations": "ROM hacks, soundtracks, and other non-game content are not supported by the emulator and may fail to load.", + "disabled": "Web Emulator functionality was disabled by the administrator.", + "contact": "Contact the administrator or spin up your own instance of Myrient Search." }, - "results": { - "table": { - "name": "Name", - "group": "Group", - "category": "Category", - "region": "Region", - "type": "Type", - "size": "Size", - "date": "Date", - "score": "Search Score", - "play": "Play" - } + "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}}. This information is pulled on search. Search queries may be slow until metadata has been cached in the database." + } + }, + "settings": { + "title": "Settings", + "search_columns": { + "title": "Search Columns", + "tooltip": "Selects which columns the search engine will search on." + }, + "score_multiplier": { + "title": "Search Score Multiplier", + "tooltip": "Multiplies the match score for each word found based on the category it's found in." + }, + "extras": { + "title": "Extras", + "fuzzy": { + "label": "Fuzzy Value", + "tooltip": "Value between 0.00 and 1.00 that determines the fuzzy distance (Levenshtein distance) for how closely a word needs to be considered a match. A higher value allows for less stringent matches. A value of 0 disables." + }, + "prefix": { + "label": "Allow Prefixes", + "tooltip": "Allows partial matches of words at the start of the word." + }, + "match_all": { + "label": "Match All Words", + "tooltip": "Requires all words in the search query to match." + }, + "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": "Redirects your search to the old table formatted results. These will load faster as they will ignore pulling metadata." + } + }, + "save": "Save Settings" + }, + "footer": { + "queries": "Number of Queries:", + "files": "Known Files:", + "terms": "Term Count:", + "last_crawl": "Time of Last Crawl:" + }, + "emulator": { + "loading": { + "rom": "Loading ROM...", + "downloading": "Downloading ROM", + "downloading_progress": "Downloading ROM: {{percent}}%", + "decompressing": "Decompressing the game..." }, "error": { - "title": "Error", - "message": "Something went wrong", - "details": "Error details: {{message}}", - "back_home": "Back to Home", - "go_back": "Go Back" + "loading": "Error loading game", + "no_rom": "No ROM file found in archive", + "http_error": "HTTP error! status: {{status}}" }, - "languages": { - "en": "English", - "es": "Español", - "pt": "Português", - "zh": "中文", - "ja": "日本語", - "fr": "Français", - "de": "Deutsch", - "ko": "한국어", - "pl": "Polski", - "tr": "Türkçe", - "it": "Italiano", - "romaji": "Romaji", - "hi": "हिन्दी", - "ar": "العربية", - "bn": "বাংলা", - "ru": "Русский" + "warning": { + "non_game": "Warning: This file may not be a game ROM and might not work properly in the web emulator.", + "see_about": "See the {{link}} page for more information.", + "no_data": "No emulator data available.", + "https": "Insecure HTTP: Some emulators require HTTPS to function properly. This instance isn't properly configured." + }, + "console": { + "about": "This is an online emulator running game ROMs directly from Myrient's public archive.", + "disclaimer": "No game ROMs are hosted on this website. All content is loaded directly from Myrient servers.", + "more_info": "For more information about this service, please visit the About page." + }, + "recommended": "Recommended Emulators", + "download": "Download", + "play": "Play", + "not_available": "----", + "disclaimer": "This emulator loads games directly from {{link}}. Learn more on the {{about}} page." + }, + "results": { + "table": { + "name": "Name", + "group": "Group", + "category": "Category", + "region": "Region", + "type": "Type", + "size": "Size", + "date": "Date", + "score": "Search Score", + "play": "Play" } -} \ No newline at end of file + }, + "error": { + "title": "Error", + "message": "Something went wrong", + "details": "Error details: {{message}}", + "back_home": "Back to Home", + "go_back": "Go Back" + }, + "languages": { + "en": "English", + "es": "Español", + "pt": "Português", + "zh": "中文", + "ja": "日本語", + "fr": "Français", + "de": "Deutsch", + "ko": "한국어", + "pl": "Polski", + "tr": "Türkçe", + "it": "Italiano", + "romaji": "Romaji", + "hi": "हिन्दी", + "ar": "العربية", + "bn": "বাংলা", + "ru": "Русский" + } +} diff --git a/config/locales/es.json b/config/locales/es.json index 7572129..48f5e88 100644 --- a/config/locales/es.json +++ b/config/locales/es.json @@ -21,7 +21,22 @@ "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" }, "about": { "title": "Acerca de", @@ -40,6 +55,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}}. Esta información se obtiene durante la búsqueda. Las consultas de búsqueda pueden ser lentas hasta que los metadatos se almacenen en caché en la base de datos." } }, "settings": { @@ -67,11 +86,15 @@ "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:", diff --git a/config/locales/fr.json b/config/locales/fr.json index dc877d2..155fe25 100644 --- a/config/locales/fr.json +++ b/config/locales/fr.json @@ -21,7 +21,22 @@ "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" }, "about": { "title": "À propos", @@ -40,6 +55,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 récupère les métadonnées des jeux depuis {{metadata_source}}. Ces informations sont récupérées lors de la recherche. Les requêtes de recherche peuvent être lentes jusqu'à ce que les métadonnées soient mises en cache dans la base de données." } }, "settings": { diff --git a/config/locales/hi.json b/config/locales/hi.json index 39965b4..58bbc5f 100644 --- a/config/locales/hi.json +++ b/config/locales/hi.json @@ -21,7 +21,22 @@ "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": "अधिक जानकारी" }, "about": { "title": "परिचय", @@ -40,6 +55,10 @@ "credits": { "created_by": "खोज इंजन किसके द्वारा बनाया गया:", "view_github": "GitHub पर प्रोजेक्ट देखें" + }, + "metadata": { + "title": "मेटाडेटा जानकारी", + "description": "यह वेबसाइट {{metadata_source}} से गेम्स की मेटाडेटा जानकारी प्राप्त करती है। यह जानकारी खोज के समय प्राप्त की जाती है। मेटाडेटा डेटाबेस में कैश होने तक खोज क्वेरी धीमी हो सकती हैं।" } }, "settings": { @@ -69,6 +88,10 @@ "hide_non_game": { "label": "गैर-गेम सामग्री छिपाएं", "tooltip": "खोज परिणामों से ROM हैक्स, पैच, आर्टवर्क और अन्य गैर-गेम सामग्री को फ़िल्टर करता है।" + }, + "use_old_results": { + "label": "पुरानी खोज", + "tooltip": "आपकी खोज को पुराने टेबल फॉर्मैट वाले परिणामों में भेजता है। ये मेटाडेटा को नज़रअंदाज़ करेंगे इसलिए तेज़ी से लोड होंगे।" } }, "save": "सेटिंग्स सहेजें" @@ -103,7 +126,6 @@ "more_info": "इस सेवा के बारे में अधिक जानकारी के लिए, कृपया परिचय पृष्ठ पर जाएं।" }, "recommended": "अनुशंसित एमुलेटर्स", - "download": "डाउनलोड", "play": "खेलें", "not_available": "----", "disclaimer": "यह एमुलेटर {{link}} से सीधे गेम लोड करता है। {{about}} पृष्ठ पर अधिक जानें।" diff --git a/config/locales/it.json b/config/locales/it.json index 92c4373..2e8f5bd 100644 --- a/config/locales/it.json +++ b/config/locales/it.json @@ -21,7 +21,22 @@ "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" }, "about": { "title": "Informazioni", @@ -40,6 +55,10 @@ "credits": { "created_by": "Motore di ricerca creato da", "view_github": "Visualizza progetto su GitHub" + }, + "metadata": { + "title": "Informazioni sui Metadati", + "description": "Questo sito web recupera informazioni sui metadati dei giochi da {{metadata_source}}. Queste informazioni vengono recuperate durante la ricerca. Le query di ricerca potrebbero essere lente fino a quando i metadati non vengono memorizzati nella cache del database." } }, "settings": { @@ -68,10 +87,14 @@ }, "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:", diff --git a/config/locales/ja.json b/config/locales/ja.json index 94a684a..db0750c 100644 --- a/config/locales/ja.json +++ b/config/locales/ja.json @@ -21,7 +21,22 @@ "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": "詳細情報" }, "about": { "title": "サイトについて", @@ -40,6 +55,10 @@ "credits": { "created_by": "検索エンジン開発者:", "view_github": "GitHubでプロジェクトを見る" + }, + "metadata": { + "title": "メタデータ情報", + "description": "このサイトは{{metadata_source}}からゲームのメタデータ情報を取得します。この情報は検索時に取得されます。メタデータがデータベースにキャッシュされるまで、検索クエリは遅くなる可能性があります。" } }, "settings": { diff --git a/config/locales/ko.json b/config/locales/ko.json index ef66fb4..498884e 100644 --- a/config/locales/ko.json +++ b/config/locales/ko.json @@ -21,7 +21,22 @@ "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": "자세히 보기" }, "about": { "title": "소개", @@ -40,6 +55,10 @@ "credits": { "created_by": "검색 엔진 개발자:", "view_github": "GitHub에서 프로젝트 보기" + }, + "metadata": { + "title": "메타데이터 정보", + "description": "이 웹사이트는 {{metadata_source}}에서 게임의 메타데이터 정보를 가져옵니다. 이 정보는 검색 시 가져옵니다. 메타데이터가 데이터베이스에 캐시될 때까지 검색 쿼리가 느릴 수 있습니다." } }, "settings": { @@ -103,7 +122,6 @@ "more_info": "이 서비스에 대한 자세한 정보는 소개 페이지를 확인하세요." }, "recommended": "추천 에뮬레이터", - "download": "다운로드", "play": "플레이", "not_available": "----", "disclaimer": "이 에뮬레이터는 {{link}}에서 직접 게임을 불러옵니다. {{about}} 페이지에서 더 자세히 알아보세요." diff --git a/config/locales/pl.json b/config/locales/pl.json index e14eb88..d94792d 100644 --- a/config/locales/pl.json +++ b/config/locales/pl.json @@ -21,7 +21,22 @@ "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" }, "about": { "title": "O nas", @@ -40,6 +55,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}}. Te informacje są pobierane podczas wyszukiwania. Zapytania wyszukiwania mogą być wolne, dopóki metadane nie zostaną zapisane w pamięci podręcznej bazy danych." } }, "settings": { @@ -67,8 +86,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" @@ -103,7 +126,6 @@ "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}}." diff --git a/config/locales/pt.json b/config/locales/pt.json index f439fa4..d01084d 100644 --- a/config/locales/pt.json +++ b/config/locales/pt.json @@ -21,7 +21,22 @@ "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" }, "about": { "title": "Sobre", @@ -40,6 +55,10 @@ "credits": { "created_by": "Buscador criado por", "view_github": "Ver projeto no GitHub" + }, + "metadata": { + "title": "Informações de Metadados", + "description": "Este site obtém informações de metadados sobre jogos do {{metadata_source}}. Essas informações são obtidas durante a pesquisa. As consultas de pesquisa podem ser lentas até que os metadados sejam armazenados em cache no banco de dados." } }, "settings": { diff --git a/config/locales/romaji.json b/config/locales/romaji.json index db0952f..5b31368 100644 --- a/config/locales/romaji.json +++ b/config/locales/romaji.json @@ -21,7 +21,22 @@ "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ō" }, "about": { "title": "Saito ni tsuite", @@ -40,6 +55,10 @@ "credits": { "created_by": "Kensaku enjin kaihatsusha:", "view_github": "GitHub de purojekuto wo miru" + }, + "metadata": { + "title": "Metadēta Jōhō", + "description": "Kono saito wa {{metadata_source}} kara gēmu no metadēta jōhō wo shutoku shimasu. Kono jōhō wa kensaku-ji ni shutoku saremasu. Metadēta ga dētabēsu ni kyasshu sareru made, kensaku kueri wa osoku naru kanōsei ga arimasu." } }, "settings": { @@ -103,7 +122,6 @@ "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." diff --git a/config/locales/ru.json b/config/locales/ru.json index fcca5c3..8e807ab 100644 --- a/config/locales/ru.json +++ b/config/locales/ru.json @@ -21,7 +21,22 @@ "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": "Подробнее" }, "about": { "title": "О сайте", @@ -40,6 +55,10 @@ "credits": { "created_by": "Поисковая система создана", "view_github": "Посмотреть проект на GitHub" + }, + "metadata": { + "title": "Информация о метаданных", + "description": "Этот сайт получает метаданные об играх из {{metadata_source}}. Эта информация загружается при поиске. Поисковые запросы могут быть медленными, пока метаданные не будут кэшированы в базе данных." } }, "settings": { @@ -68,7 +87,11 @@ }, "hide_non_game": { "label": "Скрыть не-игровой контент", - "tooltip": "Отфильтровывает ROM-хаки, патчи, иллюстрации и другой не-игровой контент из результатов поиска." + "tooltip": "Фильтрует ROM-хаки, патчи, изображения и другой не-игровой контент из результатов поиска." + }, + "use_old_results": { + "label": "Старый поиск", + "tooltip": "Перенаправляет ваш поиск на старый табличный формат результатов. Они загрузятся быстрее, так как не будут получать метаданные." } }, "save": "Сохранить настройки" @@ -103,7 +126,6 @@ "more_info": "Для получения дополнительной информации об этом сервисе, пожалуйста, посетите страницу О сайте." }, "recommended": "Рекомендуемые эмуляторы", - "download": "Скачать", "play": "Играть", "not_available": "----", "disclaimer": "Этот эмулятор загружает игры напрямую из {{link}}. Узнайте больше на странице {{about}}." diff --git a/config/locales/tr.json b/config/locales/tr.json index eafba86..8b40185 100644 --- a/config/locales/tr.json +++ b/config/locales/tr.json @@ -21,7 +21,22 @@ "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" }, "about": { "title": "Hakkında", @@ -40,6 +55,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 Bilgisi", + "description": "Bu web sitesi {{metadata_source}} üzerinden oyunlar hakkında meta veri bilgisi çeker. Bu bilgiler arama sırasında çekilir. Meta veriler veritabanında önbelleğe alınana kadar arama sorguları yavaş olabilir." } }, "settings": { @@ -67,8 +86,12 @@ "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" diff --git a/config/locales/zh.json b/config/locales/zh.json index 18ecf9a..2da68ef 100644 --- a/config/locales/zh.json +++ b/config/locales/zh.json @@ -21,7 +21,22 @@ "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": "更多信息" }, "about": { "title": "关于", @@ -40,6 +55,10 @@ "credits": { "created_by": "搜索引擎开发者:", "view_github": "在GitHub上查看项目" + }, + "metadata": { + "title": "元数据信息", + "description": "本网站从{{metadata_source}}获取游戏的元数据信息。此信息在搜索时获取。在元数据缓存到数据库之前,搜索查询可能会较慢。" } }, "settings": { diff --git a/views/pages/info.ejs b/views/pages/info.ejs index 52975f9..5a55932 100644 --- a/views/pages/info.ejs +++ b/views/pages/info.ejs @@ -47,40 +47,46 @@ <% } %> <% if(metadata.developers) {%>
                -

                Developed by: <%= JSON.parse(metadata.developers).join(", ") %>

                +

                <%= __('search.developed') %> <%= JSON.parse(metadata.developers).join(", ") %>

                <% } %> <% if(metadata.publishers) {%>
                -

                Published by: <%= JSON.parse(metadata.publishers).join(", ") %>

                +

                <%= __('search.published') %> <%= JSON.parse(metadata.publishers).join(", ") %>

                <% } %> <% if(metadata.releasedate) {%>
                -

                Release date: <%= metadata.releasedate %>

                +

                <%= __('search.release_date') %> <%= metadata.releasedate %>

                <% } %> <% if(file.region) {%>
                -

                Region: <%= file.region %>

                +

                <%= __('search.region') %> <%= file.region %>

                <% } %> <% if(metadata.genre) {%>
                -

                Genre: <%= JSON.parse(metadata.genre).join(", ") %>

                +

                <%= __('search.genre') %> <%= JSON.parse(metadata.genre).join(", ") %>

                <% } %> <% if(metadata.gamemodes) {%>
                -

                Gameplay modes: <%= JSON.parse(metadata.gamemodes).join(", ") %>

                +

                <%= __('search.modes') %> <%= JSON.parse(metadata.gamemodes).join(", ") %>

                <% } %> +
                +

                <%= __('search.filename') %> <%= file.filename %>

                +
                +
                +

                <%= __('search.upload_date') %> <%= file.date %>

                +

                <%= metadata.description %>

                - Download + <%= __('search.download') %> <% if (process.env.EMULATOR_ENABLED === 'true') { %> <% if (isEmulatorCompatible(file.category)) { %> <%= __('emulator.play')%> <% } else { %> diff --git a/views/pages/results.ejs b/views/pages/results.ejs index 3b3218c..ab4ad5d 100644 --- a/views/pages/results.ejs +++ b/views/pages/results.ejs @@ -6,49 +6,49 @@ %>

                -
                -
                -
                - -
                +  
                +    
                +
                + +
                 <%= generateAsciiArt() %>
                                 
                -
                - - - + + + + -
                -
                  -
                  -

                  - <%= __('search.found_plural', { count: count }) %> <%= __('search.in_seconds', { seconds: elapsed }) %>. - <%= indexing ? __('search.indexing') : "" %> - <% if (settings.hideNonGame) { %> - - <%= __('search.non_game_filter') %> - - - <% } %> -

                  - +
                  +
                    +
                    +

                    + <%= __('search.found_plural', { count: count }) %> <%= __('search.in_seconds', { seconds: elapsed }) %>. + <%= indexing ? __('search.indexing') : "" %> + <% if (settings.hideNonGame) { %> + + <%= __('search.non_game_filter') %> + + + <% } %> +

                    + -
                    -
                    - <% for (let x = 0; x < results.length; x++) { %> - <%- include("../partials/result", {result: results[x]}) %> - <% } %> -
                    - <% +
                    +
                    + <% for (let x = 0; x < results.length; x++) { %> + <%- include("../partials/result", {result: results[x]}) %> + <% } %> +
                    + <% if(pageCount > 1) { %> -
                    -
                    -
                    -
                    - <% } %> +
                    + <% } %> +
                    \ No newline at end of file diff --git a/views/partials/result.ejs b/views/partials/result.ejs index db7ae44..ac17b6b 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -10,10 +10,10 @@

                    <%= metadata.title || file.filename %>

                    <%= __('search.released') %>: <%= metadata.releasedate || file.date %> - <%= __('search.region') %>: <%= file.region %> - <%= __('search.platform') %>: <%= file.category %> + <%= __('search.region') %> <%= file.region %> + <%= __('search.platform') %> <%= file.category %> <% if(metadata.genre){ %> - Genres: <%= JSON.parse(metadata.genre).join(' / ') %> + <%= __('search.genre') %> <%= JSON.parse(metadata.genre).join(' / ') %> <% } %>

                    <% if(metadata.title) {%> @@ -22,12 +22,12 @@

                    <%= __('search.no_metadata') %>

                    <% } %> <% if(metadata.title) {%> -

                    Filename: <%= file.filename %>

                    +

                    <%= __('search.filename') %> <%= file.filename %> | <%= __('search.upload_date')%> <%= file.date %>

                    <% } %> -

                    Release Group: <%= file.group %>

                    +

                    <%= __('search.release_group') %> <%= file.group %>

                    - More Info - Download + <%= __('search.more_info') %> + <%= __('search.download') %> <% if (process.env.EMULATOR_ENABLED === 'true') { %> <% if (isEmulatorCompatible(file.category)) { %> <%= __('emulator.play')%> <% } else { %> From 35d2c5f3e359bc016268db0342d122fb7c2d8a45 Mon Sep 17 00:00:00 2001 From: Alexandra Date: Tue, 27 May 2025 20:12:30 -0600 Subject: [PATCH 23/46] update translations make new/old pages based on url parameter so that link sharing is consistent --- config/locales/ar.json | 3 ++- config/locales/bn.json | 3 ++- config/locales/de.json | 3 ++- config/locales/en.json | 3 ++- config/locales/es.json | 5 +++-- config/locales/hi.json | 3 ++- config/locales/it.json | 3 ++- config/locales/pl.json | 3 ++- config/locales/ru.json | 3 ++- config/locales/tr.json | 3 ++- server.js | 5 +++-- views/pages/index.ejs | 9 +++++++++ views/pages/info.ejs | 2 +- views/pages/resultsold.ejs | 2 +- views/pages/search.ejs | 1 + views/pages/settings.ejs | 3 --- views/partials/result.ejs | 2 +- views/public/js/navbar.js | 3 +++ views/public/js/settings.js | 3 --- 19 files changed, 40 insertions(+), 22 deletions(-) diff --git a/config/locales/ar.json b/config/locales/ar.json index b71ccba..44b8085 100644 --- a/config/locales/ar.json +++ b/config/locales/ar.json @@ -32,7 +32,7 @@ "published": "الناشر:", "developed": "المطور:", "modes": "أنماط اللعب:", - "download": "تحميل:", + "download": "تحميل", "filename": "اسم الملف:", "release_group": "مجموعة الإصدار:", "upload_date": "تاريخ الرفع:", @@ -129,6 +129,7 @@ "download": "تنزيل", "play": "تشغيل", "not_available": "----", + "not_available_tooltip": "المحاكاة عبر الويب غير متوفرة لهذا العنوان إما لأنه ليس لعبة أو لأن المنصة غير مدعومة.", "disclaimer": "يقوم هذا المحاكي بتحميل الألعاب مباشرة من {{link}}. تعرف على المزيد في صفحة {{about}}." }, "results": { diff --git a/config/locales/bn.json b/config/locales/bn.json index f23a564..fa98f31 100644 --- a/config/locales/bn.json +++ b/config/locales/bn.json @@ -32,7 +32,7 @@ "published": "প্রকাশক:", "developed": "ডেভেলপার:", "modes": "গেমপ্লে মোড:", - "download": "ডাউনলোড:", + "download": "ডাউনলোড", "filename": "ফাইলের নাম:", "release_group": "রিলিজ গ্রুপ:", "upload_date": "আপলোডের তারিখ:", @@ -129,6 +129,7 @@ "download": "ডাউনলোড", "play": "প্লে", "not_available": "----", + "not_available_tooltip": "এই টাইটেলের জন্য ওয়েব এমুলেশন উপলব্ধ নয় কারণ এটি হয় একটি গেম নয় অথবা প্ল্যাটফর্মটি সমর্থিত নয়।", "disclaimer": "এই এমুলেটর {{link}} থেকে সরাসরি গেম লোড করে। {{about}} পৃষ্ঠায় আরও জানুন।" }, "results": { diff --git a/config/locales/de.json b/config/locales/de.json index cf7eed1..838a966 100644 --- a/config/locales/de.json +++ b/config/locales/de.json @@ -32,7 +32,7 @@ "published": "Veröffentlicht von:", "developed": "Entwickelt von:", "modes": "Spielmodi:", - "download": "Herunterladen:", + "download": "Herunterladen", "filename": "Dateiname:", "release_group": "Release-Gruppe:", "upload_date": "Upload-Datum:", @@ -129,6 +129,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": { diff --git a/config/locales/en.json b/config/locales/en.json index 82dd372..b58161e 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -32,7 +32,7 @@ "published": "Published by:", "developed": "Developed by:", "modes": "Gameplay modes:", - "download": "Download:", + "download": "Download", "filename": "Filename:", "release_group": "Release group:", "upload_date": "Upload date:", @@ -129,6 +129,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": { diff --git a/config/locales/es.json b/config/locales/es.json index 48f5e88..268476f 100644 --- a/config/locales/es.json +++ b/config/locales/es.json @@ -32,7 +32,7 @@ "published": "Publicado por:", "developed": "Desarrollado por:", "modes": "Modos de juego:", - "download": "Descargar:", + "download": "Descargar", "filename": "Nombre del archivo:", "release_group": "Grupo de lanzamiento:", "upload_date": "Fecha de subida:", @@ -129,7 +129,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": { diff --git a/config/locales/hi.json b/config/locales/hi.json index 58bbc5f..333fbae 100644 --- a/config/locales/hi.json +++ b/config/locales/hi.json @@ -32,7 +32,7 @@ "published": "प्रकाशक:", "developed": "डेवलपर:", "modes": "गेमप्ले मोड:", - "download": "डाउनलोड:", + "download": "डाउनलोड", "filename": "फ़ाइल का नाम:", "release_group": "रिलीज़ ग्रुप:", "upload_date": "अपलोड की तारीख:", @@ -128,6 +128,7 @@ "recommended": "अनुशंसित एमुलेटर्स", "play": "खेलें", "not_available": "----", + "not_available_tooltip": "इस शीर्षक के लिए वेब एमुलेशन उपलब्ध नहीं है क्योंकि यह या तो एक गेम नहीं है या प्लेटफ़ॉर्म समर्थित नहीं है।", "disclaimer": "यह एमुलेटर {{link}} से सीधे गेम लोड करता है। {{about}} पृष्ठ पर अधिक जानें।" }, "results": { diff --git a/config/locales/it.json b/config/locales/it.json index 2e8f5bd..98dfc78 100644 --- a/config/locales/it.json +++ b/config/locales/it.json @@ -32,7 +32,7 @@ "published": "Pubblicato da:", "developed": "Sviluppato da:", "modes": "Modalità di gioco:", - "download": "Scarica:", + "download": "Scarica", "filename": "Nome file:", "release_group": "Gruppo di release:", "upload_date": "Data di caricamento:", @@ -129,6 +129,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": { diff --git a/config/locales/pl.json b/config/locales/pl.json index d94792d..1284ad7 100644 --- a/config/locales/pl.json +++ b/config/locales/pl.json @@ -32,7 +32,7 @@ "published": "Wydawca:", "developed": "Deweloper:", "modes": "Tryby gry:", - "download": "Pobierz:", + "download": "Pobierz", "filename": "Nazwa pliku:", "release_group": "Grupa wydania:", "upload_date": "Data przesłania:", @@ -128,6 +128,7 @@ "recommended": "Polecane emulatory", "play": "Graj", "not_available": "----", + "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}}." }, "results": { diff --git a/config/locales/ru.json b/config/locales/ru.json index 8e807ab..330aac6 100644 --- a/config/locales/ru.json +++ b/config/locales/ru.json @@ -32,7 +32,7 @@ "published": "Издатель:", "developed": "Разработчик:", "modes": "Режимы игры:", - "download": "Скачать:", + "download": "Скачать", "filename": "Имя файла:", "release_group": "Релиз-группа:", "upload_date": "Дата загрузки:", @@ -128,6 +128,7 @@ "recommended": "Рекомендуемые эмуляторы", "play": "Играть", "not_available": "----", + "not_available_tooltip": "Веб-эмуляция недоступна для этого заголовка, так как это либо не игра, либо платформа не поддерживается.", "disclaimer": "Этот эмулятор загружает игры напрямую из {{link}}. Узнайте больше на странице {{about}}." }, "results": { diff --git a/config/locales/tr.json b/config/locales/tr.json index 8b40185..4f9a19f 100644 --- a/config/locales/tr.json +++ b/config/locales/tr.json @@ -32,7 +32,7 @@ "published": "Yayıncı:", "developed": "Geliştirici:", "modes": "Oyun modları:", - "download": "İndir:", + "download": "İndir", "filename": "Dosya adı:", "release_group": "Yayın grubu:", "upload_date": "Yükleme tarihi:", @@ -129,6 +129,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": { diff --git a/server.js b/server.js index a8acf59..60f42a8 100644 --- a/server.js +++ b/server.js @@ -200,13 +200,14 @@ app.get("/search", async function (req, res) { if (settings.combineWith != "AND") { delete settings.combineWith; } + let loadOldResults = req.query.old === "true" ? true : false settings.pageSize = settings.useOldResults ? 100 : 10; settings.page = pageNum - 1; settings.sort = req.query.o || ""; let results = await search.findAllMatches(query, settings); debugPrint(results); let metas = []; - if (!settings.useOldResults) { + if (!loadOldResults) { metas = await metadataSearch.queueGetGamesMetadata(results.db); } if (results.count && pageNum == 1) { @@ -225,7 +226,7 @@ app.get("/search", async function (req, res) { urlPrefix: urlPrefix, settings: settings, }; - let page = settings.useOldResults ? "resultsold" : "results"; + let page = loadOldResults ? "resultsold" : "results"; options = buildOptions(page, options); res.render(indexPage, options); }); diff --git a/views/pages/index.ejs b/views/pages/index.ejs index 6120571..e282313 100644 --- a/views/pages/index.ejs +++ b/views/pages/index.ejs @@ -25,6 +25,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() + } + } } + + + \ No newline at end of file diff --git a/views/pages/info.ejs b/views/pages/info.ejs index 5a55932..d8e3844 100644 --- a/views/pages/info.ejs +++ b/views/pages/info.ejs @@ -90,7 +90,7 @@ <% if (process.env.EMULATOR_ENABLED === 'true') { %> <% if (isEmulatorCompatible(file.category)) { %> <%= __('emulator.play')%> <% } else { %> - + <% } }%>

                    diff --git a/views/pages/resultsold.ejs b/views/pages/resultsold.ejs index a436ef2..43c0de9 100644 --- a/views/pages/resultsold.ejs +++ b/views/pages/resultsold.ejs @@ -90,7 +90,7 @@ <% if (isEmulatorCompatible(results[x].category)) { %> <%= __('emulator.play') %> <% } else { %> - + <% } %>