mirror of https://github.com/krateng/maloja.git
Compare commits
1083 Commits
Author | SHA1 | Date |
---|---|---|
krateng | 39a42e915c | |
krateng | b8944b4954 | |
krateng | 9d9f3b500e | |
krateng | 72c58509a1 | |
krateng | 11a5cb7401 | |
krateng | b4c8a0d68b | |
krateng | 88403d2583 | |
krateng | 866d4ccd9b | |
FoxxMD | 3db51a94d6 | |
FoxxMD | a9c29f158e | |
krateng | ab8af32812 | |
FoxxMD | 7bc2ba0237 | |
FoxxMD | b8371347b7 | |
FoxxMD | 1e3c6597d4 | |
krateng | 37210995fa | |
Chris Newton | 94ae453133 | |
krateng | 93bbaac0e3 | |
krateng | 00a564c54d | |
krateng | 4330b0294b | |
krateng | b53141f065 | |
krateng | 3ae395f697 | |
krateng | 5466b6c37e | |
krateng | e85861fb79 | |
krateng | a611b78dbc | |
krateng | c3ed5f318d | |
krateng | 073448257a | |
krateng | d12229d8a5 | |
krateng | d8f53a56d2 | |
krateng | c8f9e9c391 | |
krateng | 185a5b3e87 | |
krateng | 95eaf0a3d6 | |
krateng | a7d286c90c | |
krateng | ddc78c5756 | |
krateng | a12253dc29 | |
krateng | 9eaeffca7e | |
krateng | db8389e6c1 | |
krateng | ef06f22622 | |
krateng | b333009684 | |
krateng | ebd78914f9 | |
krateng | 36d0e7bb8a | |
krateng | 91750db8ac | |
krateng | d5f2c254f3 | |
krateng | e3933e7dca | |
Karol Kosek | 9b10ca4a5d | |
Karol Kosek | 2ce2e2f682 | |
krateng | 9917210b66 | |
krateng | 5656f8b4c0 | |
badlandspray | 9ae14da397 | |
badlandspray | 3fd02c1675 | |
badlandspray | f7251c613c | |
badlandspray | d57bf33969 | |
krateng | a1b2261fa7 | |
krateng | 260c587248 | |
badlandspray | c1493255b7 | |
krateng | 97fc38f919 | |
krateng | 397d5e7c13 | |
krateng | 1eaba888c7 | |
krateng | 084c7d5a1e | |
krateng | 515fa69fce | |
krateng | ca30309450 | |
badlandspray | 705f4b4252 | |
krateng | ac498bde73 | |
krateng | f3a04c79b1 | |
krateng | f74d5679eb | |
krateng | 5eb838d5df | |
krateng | 96778709bd | |
krateng | a073930601 | |
krateng | 81f4e35258 | |
krateng | c16919eb1e | |
krateng | e116690640 | |
krateng | 8cb332b9fc | |
krateng | 3ede71fc79 | |
krateng | 77a0a0a41b | |
alim4r | ec02672a2e | |
alim4r | 5941123c52 | |
alim4r | 91a7aeb50d | |
krateng | 20aae955b2 | |
krateng | d83b44de6e | |
krateng | 8197548285 | |
krateng | 6171d1d2e1 | |
krateng | 0c948561a8 | |
krateng | 02c77a5e31 | |
krateng | bfa553bed0 | |
krateng | 3592571afd | |
krateng | c77b7c952f | |
krateng | 8a44d3def2 | |
krateng | cf04583122 | |
krateng | 8845f931df | |
krateng | 9c6c91f594 | |
krateng | 2c31df3c58 | |
krateng | 9c656ee90b | |
krateng | 938947d06c | |
krateng | ac3ca0b5e9 | |
krateng | 64d4036f55 | |
krateng | 6df363a763 | |
krateng | 7062c0b440 | |
krateng | ad50ee866c | |
krateng | 62abc31930 | |
krateng | c55e12dd43 | |
krateng | 3b156a73ff | |
krateng | 5b48c33a79 | |
krateng | 95f98370cf | |
krateng | e470e2e43f | |
krateng | 35f428ef69 | |
krateng | 342b8867d9 | |
krateng | bfc83fdbb0 | |
krateng | f359662cf3 | |
krateng | de286b58b9 | |
krateng | d5f5b48d85 | |
Jiri Travnicek | 00b3e6fc57 | |
Jiri Travnicek | e1074ba259 | |
krateng | 7c77474feb | |
krateng | 279499ad9f | |
krateng | dc1becd683 | |
krateng | b3d4cb7a15 | |
krateng | 4c1ba087ba | |
krateng | 0c94dc845b | |
Jiri Travnicek | 9589a6a5c9 | |
Jiri Travnicek | d54f2f8d35 | |
Jiri Travnicek | 082d11309b | |
krateng | 3cb72f46bc | |
krateng | d81f8374c9 | |
krateng | c86ae31ea9 | |
krateng | c3bb8ad322 | |
krateng | 6c5f08aa5a | |
krateng | 29a6a74c37 | |
krateng | 1bbb600481 | |
krateng | df07307730 | |
krateng | 74977b18cc | |
krateng | 029d0464b4 | |
krateng | db8bf60aef | |
krateng | 52ee456b1f | |
northys | 519c26b8d8 | |
krateng | e330678a05 | |
krateng | 0424fa7795 | |
krateng | 528d3565b7 | |
krateng | 56f7c18495 | |
krateng | 1dfda0086e | |
krateng | 7c9f6e9e2d | |
krateng | 529d0c8a5d | |
krateng | cf4b3cd68f | |
krateng | 9272c191d8 | |
krateng | d0ccf3d1ae | |
krateng | 10fef00592 | |
krateng | 1ed4af10ac | |
krateng | 11bc92ee8f | |
krateng | 98c791064d | |
krateng | d208290956 | |
krateng | 009d77a75e | |
krateng | e6992f1e90 | |
krateng | c52ad81fc2 | |
krateng | f5d1fbc576 | |
krateng | a8f8d86ec1 | |
krateng | e9189b8903 | |
krateng | 01d52d7e36 | |
krateng | 528c954de9 | |
krateng | 7c0ecda8a2 | |
krateng | 6e4e62755d | |
krateng | 646c290a37 | |
krateng | 28163348fa | |
alim4r | 495627f3f7 | |
alim4r | 6893fd745a | |
krateng | 91dae00851 | |
krateng | c0ff50b064 | |
krateng | 884e95dc58 | |
krateng | 8023c2d51c | |
krateng | df6bbebe31 | |
krateng | de625cc3c2 | |
krateng | 428d92a267 | |
krateng | 20092df02c | |
krateng | 713dbc34bb | |
krateng | 181406d339 | |
krateng | 9b5eb6f723 | |
krateng | 662923dd5e | |
krateng | ff71a9c526 | |
krateng | fbbd959295 | |
krateng | ce495176c1 | |
krateng | afc78e75b0 | |
alim4r | 85bb1f36cc | |
alim4r | c457b58ab8 | |
krateng | 62208bf668 | |
krateng | 53bc856222 | |
alim4r | b525252af1 | |
krateng | 43ec4c2c9e | |
krateng | 17be00f794 | |
krateng | fe21894c5e | |
krateng | 2bb3fa12b3 | |
krateng | 32a900cf37 | |
krateng | 397eaf668f | |
northys | 80ba4550c7 | |
krateng | b31e778d95 | |
krateng | 6e8cbe6a57 | |
krateng | 45ea7499b2 | |
krateng | 77c4dac7be | |
krateng | 61526fdc89 | |
krateng | ea6d70a650 | |
krateng | 57e66fdafd | |
krateng | 0d985ff706 | |
krateng | 27a9543da9 | |
krateng | c9d2527a98 | |
krateng | 977385a700 | |
krateng | c8522bd473 | |
krateng | 83e3157ad1 | |
krateng | 0525ff400b | |
krateng | 13856a2347 | |
krateng | e9bf65da34 | |
krateng | 5bf66ab270 | |
krateng | 206ebd58ea | |
krateng | a642c274e3 | |
ICTman1076 | 8ba973ed91 | |
krateng | ca726c774a | |
krateng | 33bbe61ece | |
krateng | 15f815ffe9 | |
krateng | fa2ce0c05f | |
krateng | b806be6e02 | |
krateng | 6601920f69 | |
krateng | f3f7dbd8ef | |
krateng | 263e7cd704 | |
krateng | 5b8e2debbc | |
krateng | bccd88acd4 | |
krateng | 371e73ac99 | |
krateng | c33fcf1dc1 | |
krateng | 98e1926613 | |
krateng | b255d424ee | |
krateng | 28d43d00cb | |
krateng | 7f9aa125af | |
krateng | 1d9247fc72 | |
krateng | c91cae9de1 | |
krateng | 1a977d9c0c | |
krateng | 62a654bfbf | |
krateng | 16d8ed0575 | |
krateng | 7c1d45f4af | |
krateng | 65fd57dceb | |
krateng | 29f722e3d3 | |
krateng | e6bb844ff9 | |
krateng | 4cffc9971d | |
krateng | bcb1d36b4a | |
krateng | 9d8752d052 | |
krateng | 741246a7c1 | |
Daniel Aleksandersen | c076518d76 | |
krateng | 4a8221f7a0 | |
krateng | 42579ad1f0 | |
krateng | ef312c6ba9 | |
krateng | dad1365627 | |
krateng | 1c2062c512 | |
krateng | 6b39ca8b19 | |
krateng | 700b81217c | |
krateng | 50cf592a75 | |
krateng | 0f39ecbf7e | |
krateng | d018a758c0 | |
krateng | f31c95228e | |
krateng | 2cf785faae | |
krateng | 189dfb58bc | |
krateng | cabfa298b9 | |
krateng | b8aa2a562e | |
krateng | cc4d40ae3f | |
krateng | 5a2856a682 | |
krateng | 2d2a7c2ee7 | |
krateng | 6635a9ac50 | |
krateng | df5eb499af | |
krateng | e52f35d65b | |
krateng | 97e1eae386 | |
krateng | e152a2edde | |
krateng | 871b3d289d | |
krateng | abde7e72c4 | |
krateng | 24dfa41ad9 | |
krateng | bceb0db09a | |
krateng | 87f1250629 | |
krateng | bb68afee12 | |
krateng | 233e49d087 | |
krateng | fe727dedee | |
krateng | 64f6836365 | |
krateng | 96933d5f18 | |
krateng | fba21b7128 | |
krateng | 1207475e4d | |
krateng | de5ae6408a | |
krateng | 45d481b1ed | |
krateng | 806f024f51 | |
krateng | 5952b8de4d | |
krateng | 3115d0372b | |
krateng | df996f7cb6 | |
krateng | 42cde8b647 | |
krateng | 9e7bbb6c20 | |
krateng | 28ba7b6ad0 | |
krateng | 0f59ffb288 | |
krateng | 7864c9f897 | |
krateng | 5524c0a70f | |
krateng | dc192d7444 | |
krateng | 4e33f808e4 | |
krateng | 700d99c5ae | |
krateng | 037f195803 | |
krateng | e9d8303763 | |
krateng | 387c40d18c | |
krateng | 40c0edb06f | |
krateng | 9f26cce34b | |
krateng | a142804bfe | |
krateng | 8d111b6de7 | |
krateng | 848f009774 | |
krateng | c9fa9956bb | |
krateng | 2deb5f0e36 | |
krateng | 2c73c81434 | |
krateng | c378c9301d | |
krateng | 74f6a931a4 | |
krateng | c982cbd1c4 | |
krateng | 6b4f2f713b | |
krateng | 4682914b88 | |
krateng | 781ed66357 | |
krateng | 2720dc1be5 | |
krateng | 34db81ccef | |
krateng | 6ca18b4471 | |
krateng | c676e0a5bf | |
krateng | 08bd352641 | |
krateng | de18ecff26 | |
krateng | 36f7ab1670 | |
krateng | 24c65d4acc | |
krateng | 1257768e33 | |
krateng | f4e42f9256 | |
krateng | a16c24281e | |
krateng | bd29c1e1ba | |
krateng | 6fc3a9cbf8 | |
krateng | abe658cc77 | |
krateng | 447d31b44e | |
krateng | ec5723d2b3 | |
krateng | 8ff7acfc38 | |
krateng | 0ae9091889 | |
krateng | e1ce80131a | |
krateng | b7781d27c3 | |
krateng | b41203bac7 | |
krateng | c647a57983 | |
krateng | 1b087e92db | |
krateng | 72b74eb27e | |
krateng | 2748d0e360 | |
krateng | e0af117805 | |
krateng | 153ab41ce7 | |
krateng | acc08693b3 | |
krateng | b510e52188 | |
krateng | ba5b0c8957 | |
krateng | c8f678b600 | |
krateng | 31c6fe6243 | |
krateng | b96f0cfc08 | |
krateng | ca2596cfc9 | |
krateng | c150a57090 | |
krateng | a833039ced | |
krateng | d8821efeeb | |
krateng | 3389d6c5f5 | |
krateng | 8ed3923851 | |
krateng | 608986b239 | |
krateng | 2a1f188e37 | |
krateng | 27cacbf658 | |
krateng | 3275e4ec5d | |
krateng | 5d582d39aa | |
krateng | 3108b368ef | |
krateng | 38f2173bde | |
krateng | e611d05c34 | |
krateng | 97aed7e73c | |
krateng | 634df2ffaf | |
krateng | eea2e917f5 | |
krateng | 04947cb97d | |
krateng | a598ba96de | |
krateng | 9f8e691924 | |
krateng | be4ed055ff | |
krateng | e22ef4d268 | |
krateng | c8ed894efb | |
krateng | e31c0dce57 | |
krateng | 3f098b6993 | |
krateng | fa9fee758c | |
krateng | c0bf8cb8ac | |
krateng | e7663138c1 | |
krateng | 66bd69b49e | |
krateng | fec6686ccc | |
krateng | dad027677e | |
krateng | fce450fac3 | |
krateng | 822895461e | |
krateng | 66d703b623 | |
krateng | 68fd6fe65f | |
krateng | cc24d48e65 | |
krateng | 1fff4eca6c | |
krateng | a443e6250e | |
krateng | c3e6dcd1eb | |
krateng | 634cb38dec | |
krateng | c944a3d937 | |
krateng | cc2b984080 | |
krateng | 34e0b0fd67 | |
krateng | 4dd7cf69a7 | |
krateng | 36b47368a3 | |
krateng | 30e973402b | |
krateng | 4e1b099547 | |
krateng | e3dc401ccf | |
krateng | 7b89d227a3 | |
krateng | c8e658af43 | |
krateng | 2930d40685 | |
krateng | 02e3f17594 | |
krateng | 57142bc327 | |
krateng | e398dd3ac1 | |
krateng | 48d88b208f | |
krateng | 827b05da8f | |
krateng | bdbb644d8e | |
krateng | aff56c9069 | |
krateng | af57103300 | |
krateng | a1ef5a7791 | |
krateng | fe0d06af7e | |
krateng | 86c4261a96 | |
krateng | a4e06413d8 | |
krateng | b83eee559f | |
krateng | 631fd941ec | |
krateng | c952fab440 | |
krateng | 65f3dac40a | |
krateng | 349e0bb7ea | |
krateng | 4cd16d73d3 | |
krateng | 2b75e1e50f | |
krateng | 538daeb284 | |
krateng | d38cf8d4be | |
krateng | 9f862dd89b | |
krateng | 54a73243cc | |
krateng | d3258a7e63 | |
krateng | 0f473599a7 | |
krateng | bde06deb4f | |
krateng | a367c7c573 | |
krateng | 9e4274f209 | |
krateng | dba31867c6 | |
krateng | 6b05dde7c0 | |
krateng | 7b3e1bbaa6 | |
krateng | 900ce51af0 | |
krateng | e980efa731 | |
krateng | 42607cedb7 | |
krateng | deb35ec042 | |
krateng | ef594c2546 | |
krateng | 7e62ddebf6 | |
krateng | f645f73f1f | |
krateng | bfed3604c5 | |
krateng | cf43a9221a | |
krateng | 4c40fb0577 | |
krateng | 765ab493cb | |
krateng | a1f8e96ae4 | |
krateng | cc060d650b | |
krateng | d9f4021342 | |
krateng | b95d1e8b0c | |
krateng | 73564eccc1 | |
krateng | b53df53c40 | |
krateng | fee94a88c5 | |
krateng | 78c50d24d9 | |
krateng | 055dca4b6d | |
krateng | 8db87bdbc5 | |
krateng | a64d3610d3 | |
krateng | 034f8b32c7 | |
krateng | 38e2a184af | |
krateng | d3797d89fa | |
krateng | 4f7e1decd3 | |
krateng | b3002b1578 | |
krateng | efad4379be | |
krateng | b325fab698 | |
krateng | eb9d29686b | |
krateng | b50afe70ea | |
krateng | eb9cd4aba4 | |
krateng | df07dd7b00 | |
krateng | 7021099e7b | |
krateng | 1df51748b6 | |
krateng | 632905a1c7 | |
krateng | 65a076c249 | |
krateng | 6611ca8705 | |
krateng | c120850d42 | |
krateng | 02ddeb4dc0 | |
krateng | f68fe04760 | |
krateng | 11bebce807 | |
krateng | 1824a8e5dc | |
krateng | 8a96a2c144 | |
krateng | 44a124e6ec | |
krateng | 80acf6275f | |
krateng | 40e733a054 | |
krateng | 9fc838e4c8 | |
krateng | 2f7f4c8567 | |
krateng | 03186bc49f | |
krateng | f88852ee6a | |
krateng | 0dd6cd9dd5 | |
krateng | 8ab42b844b | |
krateng | 03dd902e1b | |
krateng | c826b069e4 | |
krateng | 0233adedec | |
krateng | 9eb8dc0b47 | |
krateng | 68a450672e | |
krateng | ccbb3d3a80 | |
krateng | f16334aaac | |
krateng | 9370e62a47 | |
krateng | 7d979e92fe | |
krateng | b5bf87bf6b | |
krateng | 4c9f824bbd | |
krateng | e62c637aa0 | |
krateng | b8fc3db371 | |
krateng | 6ac86d3d19 | |
krateng | 9ec52806c4 | |
krateng | 3714aef878 | |
krateng | eff806bd73 | |
krateng | 21d1643988 | |
krateng | 868b8396a0 | |
krateng | f806fb8ed2 | |
krateng | cd8e0ff90a | |
krateng | fb2dff8add | |
krateng | 1b0e3ffdb2 | |
krateng | f71bd72825 | |
krateng | 1e3ea1fba9 | |
krateng | 57c090bdcd | |
krateng | fe1ed955cd | |
krateng | 37bac06735 | |
krateng | b704aa8092 | |
krateng | 9b23c3bd57 | |
krateng | 83f1956256 | |
krateng | af569ae983 | |
krateng | 03e741c0ae | |
krateng | f56e23db1e | |
krateng | 84e8ac0139 | |
krateng | 6c7e94fc20 | |
krateng | 0ec93d7d0f | |
krateng | 5228a12e3f | |
krateng | cdd762a07a | |
krateng | 61b1271491 | |
krateng | c82936ad4a | |
krateng | 6aee8c7a48 | |
krateng | 115be57651 | |
krateng | 898a9930bb | |
krateng | 2a642c5f80 | |
krateng | 7f650e604e | |
krateng | a1ba5f58b8 | |
krateng | 6fc2c1c889 | |
krateng | 1be4e50b49 | |
krateng | f2dd5862e2 | |
krateng | 5651626c39 | |
krateng | 73a6c18b17 | |
krateng | c745d4a647 | |
krateng | c98fc592a1 | |
krateng | e53588b402 | |
krateng | 9f4041de78 | |
krateng | 0f40ebbad9 | |
krateng | 8661d9f7e4 | |
krateng | 83ad648832 | |
krateng | 6798500398 | |
krateng | 0d2599fb82 | |
krateng | b6551131ba | |
krateng | 06f178b58a | |
krateng | 4aa1343cf1 | |
krateng | 36c8f2654e | |
krateng | 1e70a523b2 | |
krateng | 0ccd39ffd9 | |
krateng | 2b55e3a7c9 | |
krateng | 40648b66f3 | |
krateng | 0f5ccd4645 | |
krateng | cefed03bc9 | |
krateng | 8555b28fbc | |
krateng | e4b63bb570 | |
krateng | 3ee68e75ac | |
krateng | 2fa9eeaa07 | |
krateng | 72941558d2 | |
krateng | 14938d8fb5 | |
krateng | 5ba455dc92 | |
krateng | ca78463989 | |
krateng | 7299c2e07e | |
krateng | 97e8d5b18d | |
krateng | e94607dc2c | |
krateng | e006a10f70 | |
krateng | ef04c98ea7 | |
krateng | b212e6b921 | |
krateng | 77667e7066 | |
krateng | 5a2ec61d66 | |
krateng | ba96c8e14f | |
krateng | 1ec283f885 | |
krateng | df22a595b8 | |
krateng | 6ccbf68923 | |
krateng | 4186171b8f | |
krateng | b39e2b889a | |
krateng | ed309dd36e | |
FoxxMD | 27cc4171fe | |
krateng | de05090d7d | |
krateng | b5999dab1f | |
krateng | 54ad3f6d3b | |
krateng | 647e942a35 | |
krateng | 7774d9a936 | |
krateng | 3d573b10f9 | |
krateng | a24e26678a | |
krateng | c0c474b473 | |
krateng | 319519b7e2 | |
FoxxMD | a988f0e6dc | |
krateng | 1303891a2d | |
krateng | 1a689d3a1c | |
krateng | 8958eb1b54 | |
krateng | e597ba8504 | |
krateng | 975e57ea46 | |
FoxxMD | e582e5abe2 | |
krateng | d65def2ddf | |
FoxxMD | a71086c10d | |
krateng | 09e484021c | |
krateng | 383c24511f | |
krateng | 98ec5885e7 | |
krateng | 86f8d5a3eb | |
krateng | a85ec372f2 | |
krateng | 05460f97b7 | |
krateng | 3b8723790a | |
krateng | acace4d04a | |
krateng | 59eaa2264a | |
krateng | a52c494e4b | |
krateng | d23da91101 | |
krateng | 56eaa8a793 | |
krateng | e17002299b | |
krateng | e110ed765f | |
krateng | e901ca719f | |
krateng | 83063ba943 | |
krateng | b4230c0ae6 | |
krateng | 6d5b306d93 | |
krateng | a41aa0962b | |
krateng | 898dd9735c | |
krateng | 2101223440 | |
krateng | be7780a0c0 | |
krateng | ff8e5ec8ff | |
krateng | e236cce86c | |
krateng | d9cd546952 | |
krateng | 29f8c10167 | |
krateng | 26dfdfb569 | |
krateng | 817d98e467 | |
krateng | 504bfb405e | |
krateng | 9b98b3db3c | |
krateng | 33af60ed2c | |
krateng | 5157ce825e | |
Brian Pepple | d1b598a32b | |
krateng | c31770a34c | |
krateng | d3e45b138b | |
krateng | 08cc32ac33 | |
krateng | 4b49ffb3cb | |
krateng | f50728d2a6 | |
krateng | a55343f442 | |
krateng | 01fbc8c3d3 | |
krateng | 9037403777 | |
krateng | 49598e914f | |
krateng | 738a2d84e5 | |
krateng | 79716debf8 | |
krateng | 947614ddcf | |
Timur Demin | ced5e54074 | |
krateng | 8ebd27ab76 | |
krateng | 5455abd0d1 | |
krateng | a652a22a96 | |
krateng | e664a93ef0 | |
krateng | 1727e0c92d | |
krateng | 1aeb72fd8f | |
Krateng | 0cfdc60111 | |
Krateng | 2903a88096 | |
krateng | b75b643f6b | |
krateng | 642cc7c00b | |
Krateng | 6199fc8204 | |
Krateng | 23cc1ac341 | |
Krateng | c2f8ecc2df | |
Krateng | 6555eea887 | |
Krateng | f67f900dae | |
Krateng | 49b0a57581 | |
Krateng | 06c32e205e | |
Krateng | 2a5d9498d1 | |
Krateng | 5006ad2bf1 | |
Krateng | bcd62c16fa | |
Krateng | c4a9c6dc0f | |
Krateng | a42ed56d2d | |
Krateng | 6efe4a48c7 | |
Krateng | 5bfd1e49dd | |
Krateng | c86f3597fd | |
Krateng | 723efcb8ba | |
Krateng | adfd6d2fc2 | |
Krateng | b53052c818 | |
Krateng | 4d06a327e9 | |
Krateng | ac8abd4cb6 | |
Krateng | f5f73d9223 | |
Krateng | db80e1791a | |
Krateng | f44c3fecb2 | |
Krateng | 0818dccac4 | |
Krateng | fe2b0dd7c8 | |
Krateng | ce17f77cfd | |
Krateng | 5fe4c5cd22 | |
Krateng | 218313f80c | |
Krateng | 4e69a6119d | |
krateng | 8b5254e4c7 | |
Krateng | 64bb235011 | |
Krateng | 772f207b7e | |
Krateng | f6b9f2b9fa | |
Krateng | 212fbf368e | |
Krateng | f62fd254dd | |
Krateng | 7a3ee26c87 | |
Krateng | 3de4059f85 | |
FoxxMD | 75d8251a29 | |
Krateng | b9feebb064 | |
Krateng | 74dc54862e | |
Krateng | e6535eb6bc | |
Krateng | 951f6bb562 | |
Krateng | e475b0c716 | |
Krateng | e1c9d525f5 | |
FoxxMD | c87dc32455 | |
krateng | 4304aab8b7 | |
FoxxMD | 5fa1396ddc | |
krateng | 58050513af | |
krateng | 9cdc2cf103 | |
FoxxMD | bd2a3d46ae | |
Krateng | 034bd064f1 | |
Krateng | 755567549c | |
Krateng | b84712ef22 | |
Krateng | 3cf0dd9767 | |
Krateng | cfec8e089b | |
Krateng | c216aa5f24 | |
Krateng | b38267a09f | |
Krateng | df6ad4f073 | |
Krateng | 5b7d1fd8e9 | |
Krateng | 9a6617b4b1 | |
Krateng | e70cb3e037 | |
krateng | 8a0d15d86e | |
Krateng | ac29f9728e | |
Krateng | 79d7af5025 | |
Krateng | f555ee9d9f | |
Krateng | 663c6a0372 | |
krateng | 1940f62260 | |
Krateng | 724bfd7164 | |
Krateng | e8c19a05e4 | |
Krateng | 8cf93c3100 | |
Krateng | 5c2928e13b | |
krateng | 27c65703bc | |
krateng | 0f10f278b5 | |
Krateng | cba9d839a2 | |
Krateng | ce9d882856 | |
Krateng | c125cbc20d | |
Krateng | 2705c524f9 | |
Krateng | 54c3eb9cc0 | |
Krateng | 9f63504633 | |
Krateng | 9e03a01e70 | |
Krateng | 90d6e67e97 | |
krateng | 21b27d17c5 | |
krateng | 38d4716dc5 | |
krateng | 05fca5c7c0 | |
ICTman1076 | a4722f9e55 | |
ICTman1076 | fcee13214b | |
ICTman1076 | 258b9c22d6 | |
ICTman1076 | 30f10ba46e | |
Павло | 951c192adb | |
Krateng | cbd93d2b33 | |
Krateng | 22172d8b57 | |
Krateng | c312608f2d | |
Krateng | f7861c44b4 | |
Krateng | 85f19a8b72 | |
Krateng | 13f2746171 | |
Krateng | 616c24fb5b | |
krateng | b9d9f0ff03 | |
Krateng | f0cbe33f6a | |
Zack Boehm | 026415c7d8 | |
Krateng | 5e93eb15f4 | |
Krateng | 98830b0803 | |
Krateng | 8250c7b467 | |
Krateng | 6dd3fe5512 | |
Krateng | eb9763eb65 | |
Krateng | 6466b48796 | |
Krateng | 336e36fb79 | |
Krateng | e0b990578e | |
Krateng | 2da9f154be | |
Krateng | 6316e45265 | |
Johannes Krattenmacher | b4d224fb66 | |
Krateng | 60a06efad8 | |
Krateng | a462faf2bf | |
Krateng | 160e393a00 | |
Krateng | f4a563f080 | |
Krateng | c1a4d5a4ee | |
Krateng | 9fb352cc6f | |
Krateng | 09d3f10383 | |
Krateng | a0a8e5bcc8 | |
Krateng | e8a87cb8a5 | |
Krateng | 7c8b0dd7cb | |
Krateng | e8c316f199 | |
Krateng | 5cf7ca2e9b | |
Krateng | 52a9faae90 | |
Krateng | 8b4e9609e9 | |
Krateng | 9d3ffae45c | |
Krateng | 35616212ff | |
Krateng | e4139369fe | |
Krateng | 19c928773d | |
Krateng | 03c3202cdf | |
Krateng | 79544044be | |
Krateng | 5a548f1979 | |
Krateng | 785e6cfa17 | |
Krateng | 0b54999a1c | |
Krateng | dad75dbbc2 | |
Krateng | 879b3cf170 | |
Krateng | a4812a66da | |
Krateng | f61804b095 | |
Krateng | 8acf2ef503 | |
Krateng | a6088ec7b7 | |
Krateng | ed1c595e20 | |
Krateng | 26f6a1af58 | |
Krateng | 7bc70ed0bd | |
Krateng | f7f1b1225e | |
Krateng | 10f636e7ed | |
Krateng | 01e555172f | |
Krateng | 6acab324db | |
Krateng | e27a83bdc9 | |
Krateng | 1321fcb45e | |
Krateng | 25661f82af | |
Krateng | 4a811932ac | |
Krateng | 6ca88685bf | |
Krateng | f7ca7b0dc9 | |
Krateng | 363c431b93 | |
krateng | d85d13ce5a | |
Krateng | 316ad48ae6 | |
ICTman1076 | d788b14190 | |
Krateng | b4f84625bc | |
Krateng | e5536ba384 | |
Krateng | d3c3c1fc4c | |
Krateng | e55975514f | |
Krateng | 22ee6bf751 | |
Krateng | a5edc113c8 | |
Krateng | 65861d4c41 | |
Krateng | 44a2739a3b | |
Krateng | c6adc90d4b | |
Krateng | a45696ab62 | |
Krateng | 0abf2aae39 | |
Krateng | 7fc879f778 | |
Krateng | bc5f11d499 | |
Krateng | fddbfb6a41 | |
Krateng | fb04dd507c | |
Krateng | 75bd823ad0 | |
Krateng | 3a4769cfb2 | |
Krateng | 379ee49f1c | |
Krateng | 47087b4288 | |
Krateng | fa05c40660 | |
Krateng | 439d12d87f | |
Krateng | 3e6bcc45d5 | |
Krateng | 7693ba3a20 | |
Krateng | 1563a15abd | |
Krateng | 4113d1761e | |
Krateng | 0127a25ce5 | |
Krateng | 6885fbdecc | |
Krateng | 8d7fb9a2c8 | |
Krateng | 833048440c | |
Krateng | 7f3b7031ac | |
Krateng | 2484015261 | |
Krateng | 94794bff5b | |
Krateng | 6050e26f7a | |
Krateng | ae0da83a9c | |
Krateng | 9a1bc8be03 | |
Krateng | a88afe40ec | |
Krateng | a103c360d3 | |
Krateng | 3e1331b0e3 | |
Krateng | cb7a6d2241 | |
Krateng | bdfb2a4a0b | |
Krateng | ddbdc7ec56 | |
Krateng | 0bdc4654bf | |
Krateng | 0ee6e761da | |
Krateng | 87cdb9987e | |
Krateng | 0fdd7669cc | |
Krateng | 02e41ccbe0 | |
Krateng | 7a4df06090 | |
Krateng | c44e14d0a6 | |
Krateng | ba701a2317 | |
Krateng | b5b09c4052 | |
Krateng | 0ddb5a4dd9 | |
Krateng | a0a8ba4052 | |
Krateng | 2c754c75ce | |
Krateng | 56cc06d905 | |
Krateng | 5f8e73e6c7 | |
Krateng | 27f3ff6d08 | |
Krateng | dd3c83920b | |
Krateng | 1eae55e3bb | |
Krateng | b161da1c1a | |
Krateng | 331374e35c | |
Krateng | 29f88539b4 | |
Krateng | fe106a3227 | |
Krateng | b6a66ff2ed | |
Krateng | 4c30ff5fa2 | |
Krateng | a097d34f10 | |
Krateng | f89dcf0599 | |
Krateng | abef221435 | |
Krateng | d911a7a8c4 | |
Krateng | 8659f98935 | |
Krateng | bda279d01d | |
Krateng | 990131f546 | |
Krateng | 895fcfd8c3 | |
Krateng | 00be885847 | |
Krateng | 4d10276cc1 | |
Krateng | 5f29cea6ad | |
Krateng | daa256fc3b | |
Krateng | 398b737781 | |
Krateng | be79dc1888 | |
Krateng | d48ffc964d | |
Krateng | 6c2eac550b | |
Krateng | 1835243678 | |
Krateng | 57403a89ab | |
Krateng | 4c6b40e42f | |
Krateng | 6658165bae | |
Krateng | d551513733 | |
Krateng | 5c6a901f51 | |
Krateng | 92e6fea00f | |
Krateng | 1828bd35bb | |
Krateng | e73e047af9 | |
krateng | 7f2a8f3df3 | |
Robin | e531dc4007 | |
Krateng | 08fe4695f6 | |
krateng | ef2a2c817e | |
Krateng | 813dee8400 | |
Krateng | 31585ec646 | |
Krateng | 2f67f427f2 | |
Krateng | 2df77f89e9 | |
Krateng | b21b27bb6e | |
Krateng | 98c1527f77 | |
Krateng | c166620d5f | |
Krateng | 65f9c88da4 | |
Krateng | 9b787fa3b1 | |
Krateng | d989134e65 | |
Krateng | b117e6f7ec | |
Krateng | 6aa65bf1ce | |
Krateng | b8de507a4f | |
Krateng | bf6c00fde0 | |
Krateng | 471a61f788 | |
Krateng | 2cd5472751 | |
krateng | 85f03e443e | |
Krateng | b3f4fc1246 | |
Krateng | 1a64641fe6 | |
krateng | 5103625078 | |
Krateng | afe01c8341 | |
Krateng | b17060184b | |
Krateng | 0ceb70b27e | |
Krateng | b611387011 | |
krateng | fb882a1af4 | |
Krateng | a4f13f6923 | |
Krateng | eb82282e58 | |
Krateng | 9cf1fb3ed8 | |
Krateng | 5a08fd78c6 | |
Krateng | fb51f61597 | |
Krateng | 7c6e2ad60f | |
Krateng | 8793b149f5 | |
Krateng | 55c68b21cd | |
Krateng | 83f73758c4 | |
krateng | a97a5e2fbf | |
Krateng | d4b66ec673 | |
Krateng | c6deb15437 | |
Krateng | 23e076f93a | |
Krateng | 779436bc18 | |
Krateng | e397e644cb | |
Krateng | 75aa8f86bb | |
Krateng | c285e0b024 | |
Krateng | 218c7eb2fd | |
Marco Aceti | 2004f7690f | |
Marco Aceti | 98d80d3d11 | |
Krateng | a2cc27ddd4 | |
Krateng | c518627670 | |
Krateng | fbce600c4e | |
Krateng | 95534f0f4a | |
Krateng | 2ae293a889 | |
Krateng | 33cea26a79 | |
Krateng | a6724b9455 | |
Krateng | 137da60ab9 | |
Krateng | a4abf383a6 | |
Krateng | ec914d1b40 | |
Krateng | e2f627a1a0 | |
Krateng | f9672a918d | |
Krateng | 94a20a8818 | |
Krateng | 582c4c8814 | |
Krateng | d68393f91d | |
Krateng | e85049af64 | |
Krateng | e50cce28fa | |
Krateng | dbc23ca73c | |
Krateng | 2029e5d522 | |
Krateng | e856130e19 | |
Krateng | ab28d00052 | |
Krateng | af2bd35d74 | |
Krateng | 148b3d83f8 | |
Krateng | 6369cbbeb8 | |
Krateng | c95ce17451 | |
Krateng | a06149df15 | |
Krateng | c0ecbfb28f | |
Krateng | 091c86a7ce | |
Krateng | 68000067bc | |
Krateng | 8cf39adc91 | |
Krateng | 905c6e8e02 | |
Krateng | b87379ed98 | |
Krateng | 14fb4b4023 | |
Krateng | 592e26f1ca | |
Krateng | 5d066d9b26 | |
Krateng | a6b1a8a144 | |
Krateng | 414f530035 | |
Krateng | 246f61ee14 | |
Krateng | 01bf88f83d | |
Krateng | c99ccd0586 | |
Krateng | e2c7e57918 | |
Krateng | 2ef5e88299 | |
Krateng | 661473b482 | |
Krateng | fb55944147 | |
Krateng | 24267eb31d | |
Krateng | 0f4c7b04f7 | |
Krateng | 3382c20cb4 | |
Krateng | 11eb57ed2f | |
Krateng | d11a1ea9c2 | |
Krateng | 1316026999 | |
Krateng | 0bf6e80e07 | |
Krateng | fd0033e1c0 | |
Krateng | c74cb7010f | |
Krateng | 2e69ef7df9 | |
Krateng | 47eeb38edf | |
Krateng | 75ef7a4b0c | |
Krateng | 20908dfbaa | |
Krateng | 2e4e206695 | |
Krateng | f942979e21 | |
Krateng | 8f53839db8 | |
Krateng | 55621ef4ef | |
Krateng | 3c12462d36 | |
Krateng | 3ebde4a187 | |
Krateng | 1c9fdedbed | |
Krateng | ab7ca8f192 | |
Krateng | 6372d2d8f2 | |
Krateng | 899f1ea121 | |
Krateng | aafbfa747a | |
Krateng | a5b407d92f | |
Krateng | e1eacbb272 | |
Krateng | f540862881 | |
Krateng | 9b7bc5e38e | |
Krateng | 204712e240 | |
Krateng | e6b282a1f2 | |
Krateng | f064dd602e | |
Krateng | a61208a2f8 | |
Krateng | 36754ade85 | |
Krateng | 306834cfc0 | |
Krateng | 83137433bd | |
Krateng | 3746111ee8 | |
Krateng | b955777637 | |
Krateng | 748964c8ef | |
Krateng | 580350c1d6 | |
Krateng | a8a15826c3 | |
Krateng | 4ffeb9eae2 | |
Krateng | fe953d07b1 | |
Krateng | 2170350315 | |
Krateng | f4a3fff848 | |
Krateng | 96d1b8d77b | |
Krateng | 243362d7d6 | |
Krateng | 1f90b34d95 | |
Krateng | 5342003fa4 | |
Krateng | 4f9579b8d2 | |
Krateng | af90e2633c | |
Krateng | 59e38e24ee | |
Krateng | ed62cbfc0b | |
Krateng | 1320bf4f5b | |
krateng | d8db927c8e | |
Zack Boehm | 8ad625ef72 | |
Krateng | 852795cecd | |
krateng | 731653b964 | |
Krateng | 2efe2967f6 | |
Krateng | 390118cdd8 | |
Zack Boehm | 864e7bc46d | |
Krateng | fdbeed40f0 | |
Krateng | 974574da98 | |
Krateng | 5d9ecc0557 | |
krateng | 2101b56b8c | |
Krateng | 08d478e202 | |
Krateng | a5feec7234 | |
Krateng | 564f276496 | |
Zack Boehm | cc3f9f7fef | |
krateng | 6b566e28df | |
Krateng | 725d2308b1 | |
Krateng | 7c80256c7d | |
Krateng | 6644863ca1 | |
krateng | 793fbe4769 | |
Krateng | 3ae30a5226 | |
Krateng | 89881af8fe | |
Krateng | 6818a976a5 | |
Krateng | 40573ba4f7 | |
Krateng | 4323d0dc67 | |
Krateng | 289a1dc076 | |
Krateng | ffb12045f5 | |
Krateng | cfa0734c0a | |
Krateng | 495b00803d | |
Krateng | ae5b0fc90c | |
Krateng | c3c10170bc | |
Krateng | 728c4fed54 | |
Krateng | 92edca3521 | |
Krateng | 3d78d124ef | |
Krateng | 2cbe67f149 | |
Krateng | da76b6105e | |
Krateng | 305999cc14 | |
Krateng | 63c2214913 | |
Krateng | b9ae163132 | |
Krateng | 8cbc7a8e56 | |
Krateng | 55f0a2ceaf | |
Krateng | 66c7d9e711 | |
Krateng | 35a44f7d06 | |
Krateng | 00a8f1101c | |
Krateng | 95b4fc7426 | |
Krateng | 15970ff2c6 | |
Krateng | 7b3c72d211 | |
Krateng | ab648db336 |
|
@ -0,0 +1,8 @@
|
|||
*
|
||||
!maloja
|
||||
!container
|
||||
!Containerfile
|
||||
!requirements.txt
|
||||
!pyproject.toml
|
||||
!README.md
|
||||
!LICENSE
|
4
.doreah
4
.doreah
|
@ -1,4 +0,0 @@
|
|||
logging.logfolder = logs
|
||||
settings.files = [ "settings/default.ini" , "settings/settings.ini" ]
|
||||
caching.folder = "cache/"
|
||||
regular.autostart = false
|
|
@ -0,0 +1 @@
|
|||
custom: ["https://flattr.com/@Krateng", "https://paypal.me/krateng"]
|
|
@ -0,0 +1,36 @@
|
|||
name: Publish library to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'auxiliary/malojalib/pyproject.toml'
|
||||
# When the version updates, this file changes
|
||||
# False positives only result in a failed push
|
||||
|
||||
jobs:
|
||||
publish_to_pypi:
|
||||
name: Push Library to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@7f80679172b057fc5e90d70d197929d454754a5a
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install build
|
||||
|
||||
- name: Change directory
|
||||
run: cd auxiliary/malojalib
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@717ba43cfbb0387f6ce311b169a825772f54d295
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@ -0,0 +1,21 @@
|
|||
name: Publish Chromium scrobbler to web store
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'auxiliary/chromium_scrobbler/maloja-scobbler/manifest.json'
|
||||
# When the version updates, this file changes
|
||||
|
||||
jobs:
|
||||
publish_to_pypi:
|
||||
name: Build and publish extension
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Push Extension to Web Store
|
||||
uses: Klemensas/chrome-extension-upload-action@1e8ede84548583abf1a2a495f4242c4c51539337
|
||||
with:
|
||||
refresh-token: '${{ secrets.GOOGLE_REFRESHTOKEN }}'
|
||||
client-id: '${{ secrets.GOOGLE_CLIENTID }}'
|
||||
file-name: './auxiliary/chromium_scrobbler/maloja-scrobbler.zip'
|
||||
app-id: 'cfnbifdmgbnaalphodcbandoopgbfeeh'
|
||||
publish: true
|
|
@ -0,0 +1,76 @@
|
|||
name: Build and release docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@f2a13332ac1ce8c0a71aeac48a150dbb1838ab67
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository_owner }}/maloja
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
flavor: |
|
||||
latest=true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
|
||||
with:
|
||||
context: .
|
||||
file: Containerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
|
||||
# Temp fix
|
||||
# https://github.com/docker/build-push-action/issues/252
|
||||
# https://github.com/moby/buildkit/issues/1896
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
|
||||
- name: Update Readme and short description
|
||||
uses: peter-evans/dockerhub-description@836d7e6aa8f6f32dce26f5a1dd46d3dc24997eae
|
||||
continue-on-error: true
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }}
|
||||
repository: krateng/maloja
|
||||
short-description: ${{ github.event.repository.description }}
|
|
@ -0,0 +1,31 @@
|
|||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
publish_to_pypi:
|
||||
name: Push Package to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@7f80679172b057fc5e90d70d197929d454754a5a
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install build
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@717ba43cfbb0387f6ce311b169a825772f54d295
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@ -1,19 +1,14 @@
|
|||
# generic temporary / dev files
|
||||
# temporary / generated files
|
||||
*.pyc
|
||||
*.sh
|
||||
*.note
|
||||
|
||||
# environments / builds
|
||||
.venv/*
|
||||
testdata*
|
||||
/dist
|
||||
/build
|
||||
/*.egg-info
|
||||
|
||||
# dev files
|
||||
*.xcf
|
||||
nohup.out
|
||||
/.dev
|
||||
|
||||
# user files
|
||||
*.tsv
|
||||
*.rulestate
|
||||
*.log
|
||||
|
||||
# currently not using
|
||||
/screenshot*.png
|
||||
/proxyscrobble.py
|
||||
|
||||
# only for development, normally external
|
||||
/doreah
|
||||
*.note
|
||||
*-old
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
# Scrobbling
|
||||
|
||||
Scrobbling can be done with the native API, see [below](#submitting-a-scrobble).
|
||||
In order to scrobble from a wide selection of clients, you can also use Maloja's standard-compliant APIs with the following settings:
|
||||
|
||||
GNU FM |
|
||||
------ | ---------
|
||||
Gnukebox URL | Your Maloja URL followed by `/apis/audioscrobbler`
|
||||
Username | Doesn't matter
|
||||
Password | Any of your API keys
|
||||
|
||||
ListenBrainz |
|
||||
------ | ---------
|
||||
API URL | Your Maloja URL followed by `/apis/listenbrainz`
|
||||
Username | Doesn't matter
|
||||
Auth Token | Any of your API keys
|
||||
|
||||
Audioscrobbler v1.2 |
|
||||
------ | ---------
|
||||
Server URL | Your Maloja URL followed by `/apis/audioscrobbler_legacy`
|
||||
Username | Doesn't matter
|
||||
Password | Any of your API keys
|
||||
|
||||
| :warning: | Note that these are the base URLs - some scrobblers ask you for the full endpoint instead. |
|
||||
|---------------|:------------------------|
|
||||
|
||||
## Scrobbling Guideline
|
||||
|
||||
Maloja makes no assumptions about scrobbling behaviour. The clients should decide when and whether a play is scrobbled - the server will accept it as long as it contains all necessary data. However, a general guideline is:
|
||||
|
||||
* As soon as a track has been played for 50% of its length or 4 minutes, it should be counted as a scrobble
|
||||
* That scrobble should be submitted when the play has ended in order to know its duration
|
||||
* If the total play duration is enough to count as a scrobble, but not longer than the total track length + enough for a second scrobble, it should be submitted as a scrobble with the according duration
|
||||
* If the duration exceeds this value, the first scrobble should be submitted as a scrobble with the duration of the full track length, while the second scrobble is queued up following the above suggestions in regards to remaining time
|
||||
|
||||
|
||||
<table>
|
||||
<tr><td>:memo: Example </td><tr>
|
||||
<tr><td>
|
||||
|
||||
The user starts playing '(Fine Layers of) Slaysenflite', which is exactly 3:00 minutes long.
|
||||
* If the user ends the play after 1:22, no scrobble is submitted
|
||||
* If the user ends the play after 2:06, a scrobble with `"duration":126` is submitted
|
||||
* If the user jumps back several times and ends the play after 3:57, a scrobble with `"duration":237` is submitted
|
||||
* If the user jumps back several times and ends the play after 4:49, two scrobbles with `"duration":180` and `"duration":109` are submitted
|
||||
|
||||
</td></tr>
|
||||
<table>
|
||||
|
||||
|
||||
# API Documentation
|
||||
|
||||
The native Maloja API is reachable at `/apis/mlj_1`. Endpoints are listed on `/api_explorer`.
|
||||
|
||||
All endpoints return JSON data. POST request can be made with query string or form data arguments, but this is discouraged - JSON should be used whenever possible.
|
||||
|
||||
No application should ever rely on the non-existence of fields in the JSON data - i.e., additional fields can be added at any time without this being considered a breaking change. Existing fields should usually not be removed or changed, but it is always a good idea to add basic handling for missing fields.
|
||||
|
||||
## Submitting a Scrobble
|
||||
|
||||
The POST endpoint `/newscrobble` is used to submit new scrobbles. These use a flat JSON structure with the following fields:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `artists` | List(String) | Track artists |
|
||||
| `title` | String | Track title |
|
||||
| `album` | String | Name of the album (Optional) |
|
||||
| `albumartists` | List(String) | Album artists (Optional) |
|
||||
| `duration` | Integer | How long the song was listened to in seconds (Optional) |
|
||||
| `length` | Integer | Actual length of the full song in seconds (Optional) |
|
||||
| `time` | Integer | Timestamp of the listen if it was not at the time of submitting (Optional) |
|
||||
| `nofix` | Boolean | Skip server-side metadata fixing (Optional) |
|
||||
|
||||
## General Structure
|
||||
|
||||
The API is not fully consistent in order to ensure backwards-compatibility. Refer to the individual endpoints.
|
||||
Generally, most endpoints follow this structure:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `status` | String | Status of the request. Can be `success`, `ok`, `error`, `failure`, `no_operation` |
|
||||
| `error` | Mapping | Details about the error if one occured. |
|
||||
| `warnings` | List | Any warnings that did not result in failure, but should be noted. Field is omitted if there are no warnings! |
|
||||
| `desc` | String | Human-readable feedback. This can be shown directly to the user if desired. |
|
||||
| `list` | List | List of returned [entities](#entity-structure) |
|
||||
|
||||
|
||||
Both errors and warnings have the following structure:
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `type` | String | Name of the error or warning type |
|
||||
| `value` | varies | Specific data for this error or warning instance |
|
||||
| `desc` | String | Human-readable error or warning description. This can be shown directly to the user if desired. |
|
||||
|
||||
|
||||
## Entity Structure
|
||||
|
||||
Whenever a list of entities is returned, they have the following fields:
|
||||
|
||||
### Scrobble
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `time` | Integer | Timestamp of the Scrobble in UTC |
|
||||
| `track` | Mapping | The [track](#track) being scrobbled |
|
||||
| `duration` | Integer | How long the track was played for in seconds |
|
||||
| `origin` | String | Client that submitted the scrobble, or import source |
|
||||
|
||||
|
||||
<table>
|
||||
<tr><td>:memo: Example </td><tr>
|
||||
<tr><td>
|
||||
|
||||
```json
|
||||
{
|
||||
"time": 1650684324,
|
||||
"track": {
|
||||
"artists": ["Jennie Kim","HyunA","LE","SunMi"],
|
||||
"title": "Wow Thing",
|
||||
"length":200
|
||||
},
|
||||
"duration": 196,
|
||||
"origin": "client:navidrome_desktop"
|
||||
}
|
||||
```
|
||||
|
||||
</tr></td>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
### Track
|
||||
|
||||
| Key | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `artists` | List | The [artists](#artist) credited with the track |
|
||||
| `title` | String | The title of the track |
|
||||
| `length` | Integer | The full length of the track in seconds |
|
||||
|
||||
<table>
|
||||
<tr><td>:memo: Example </td><tr>
|
||||
<tr><td>
|
||||
|
||||
```json
|
||||
{
|
||||
"artists": ["Blackpink","Chou Tzuyu"],
|
||||
"title": "MORE",
|
||||
"length": 171
|
||||
}
|
||||
```
|
||||
|
||||
</tr></td>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
### Artist
|
||||
|
||||
Artists are just represented as raw Strings.
|
||||
|
||||
**Example**
|
||||
|
||||
<table>
|
||||
<tr><td>:memo: Example </td><tr>
|
||||
<tr><td>
|
||||
|
||||
```json
|
||||
"Red Velvet"
|
||||
```
|
||||
|
||||
</tr></td>
|
||||
</table>
|
|
@ -0,0 +1,36 @@
|
|||
# Contributor: Johannes Krattenmacher <maloja@dev.krateng.ch>
|
||||
# Maintainer: Johannes Krattenmacher <maloja@dev.krateng.ch>
|
||||
pkgname=maloja
|
||||
pkgver=3.0.0-dev
|
||||
pkgrel=0
|
||||
pkgdesc="Self-hosted music scrobble database"
|
||||
url="https://github.com/krateng/maloja"
|
||||
arch="noarch"
|
||||
license="GPL-3.0"
|
||||
depends="python3 tzdata"
|
||||
pkgusers=$pkgname
|
||||
pkggroups=$pkgname
|
||||
depends_dev="gcc g++ python3-dev libxml2-dev libxslt-dev libffi-dev libc-dev py3-pip linux-headers"
|
||||
makedepends="$depends_dev"
|
||||
source="
|
||||
$pkgname-$pkgver.tar.gz::https://github.com/krateng/maloja/archive/refs/tags/v$pkgver.tar.gz
|
||||
"
|
||||
builddir="$srcdir"/$pkgname-$pkgver
|
||||
|
||||
|
||||
|
||||
build() {
|
||||
cd $builddir
|
||||
python3 -m build .
|
||||
pip3 install dist/*.tar.gz
|
||||
}
|
||||
|
||||
package() {
|
||||
mkdir -p /etc/$pkgname || return 1
|
||||
mkdir -p /var/lib/$pkgname || return 1
|
||||
mkdir -p /var/cache/$pkgname || return 1
|
||||
mkdir -p /var/logs/$pkgname || return 1
|
||||
}
|
||||
|
||||
# TODO
|
||||
sha512sums="a674eaaaa248fc2b315514d79f9a7a0bac6aa1582fe29554d9176e8b551e8aa3aa75abeebdd7713e9e98cc987e7bd57dc7a5e9a2fb85af98b9c18cb54de47bf7 $pkgname-${pkgver}.tar.gz"
|
|
@ -0,0 +1,74 @@
|
|||
FROM lsiobase/alpine:3.17 as base
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
|
||||
|
||||
COPY --chown=abc:abc ./requirements.txt ./requirements.txt
|
||||
|
||||
# based on https://github.com/linuxserver/docker-pyload-ng/blob/main/Dockerfile
|
||||
# everything but the app installation is run in one command so we can purge
|
||||
# all build dependencies and cache in the same layer
|
||||
# it may be possible to decrease image size slightly by using build stage and
|
||||
# copying all site-packages to runtime stage but the image is already pretty small
|
||||
RUN \
|
||||
echo "**** install build packages ****" && \
|
||||
apk add --no-cache --virtual=build-deps \
|
||||
gcc \
|
||||
g++ \
|
||||
python3-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
libffi-dev \
|
||||
libc-dev \
|
||||
py3-pip \
|
||||
linux-headers && \
|
||||
echo "**** install runtime packages ****" && \
|
||||
apk add --no-cache \
|
||||
python3 \
|
||||
py3-lxml \
|
||||
tzdata && \
|
||||
echo "**** install pip dependencies ****" && \
|
||||
python3 -m ensurepip && \
|
||||
pip3 install -U --no-cache-dir \
|
||||
pip \
|
||||
wheel && \
|
||||
echo "**** install maloja requirements ****" && \
|
||||
pip3 install --no-cache-dir -r requirements.txt && \
|
||||
echo "**** cleanup ****" && \
|
||||
apk del --purge \
|
||||
build-deps && \
|
||||
rm -rf \
|
||||
/tmp/* \
|
||||
${HOME}/.cache
|
||||
|
||||
# actual installation in extra layer so we can cache the stuff above
|
||||
|
||||
COPY --chown=abc:abc . .
|
||||
|
||||
RUN \
|
||||
echo "**** install maloja ****" && \
|
||||
apk add --no-cache --virtual=install-deps \
|
||||
py3-pip && \
|
||||
pip3 install /usr/src/app && \
|
||||
apk del --purge \
|
||||
install-deps && \
|
||||
rm -rf \
|
||||
/tmp/* \
|
||||
${HOME}/.cache
|
||||
|
||||
|
||||
|
||||
COPY container/root/ /
|
||||
|
||||
ENV \
|
||||
# Docker-specific configuration
|
||||
MALOJA_SKIP_SETUP=yes \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# Prevents breaking change for previous container that ran maloja as root
|
||||
# On linux hosts (non-podman rootless) these variables should be set to the
|
||||
# host user that should own the host folder bound to MALOJA_DATA_DIRECTORY
|
||||
PUID=0 \
|
||||
PGID=0
|
||||
|
||||
EXPOSE 42010
|
|
@ -0,0 +1,58 @@
|
|||
# Development
|
||||
|
||||
Clone the repository and enter it.
|
||||
|
||||
```console
|
||||
git clone https://github.com/krateng/maloja
|
||||
cd maloja
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
To avoid cluttering your system, consider using a [virtual environment](https://docs.python.org/3/tutorial/venv.html).
|
||||
|
||||
Your system needs several packages installed. For supported distributions, this can be done with e.g.
|
||||
|
||||
```console
|
||||
sh ./install/install_dependencies_alpine.sh
|
||||
```
|
||||
|
||||
For other distros, try to find the equivalents of the packages listed or simply check your error output.
|
||||
|
||||
Then install all Python dependencies with
|
||||
|
||||
```console
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
|
||||
## Running the server
|
||||
|
||||
For development, you might not want to install maloja files all over your filesystem. Use the environment variable `MALOJA_DATA_DIRECTORY` to force all user files into one central directory - this way, you can also quickly change between multiple configurations.
|
||||
|
||||
You can quickly run the server with all your local changes with
|
||||
|
||||
```console
|
||||
python3 -m maloja run
|
||||
```
|
||||
|
||||
You can also build the package with
|
||||
|
||||
```console
|
||||
pip install .
|
||||
```
|
||||
|
||||
|
||||
## Docker
|
||||
|
||||
You can also always build and run the server with
|
||||
|
||||
```console
|
||||
sh ./dev/run_docker.sh
|
||||
```
|
||||
|
||||
This will use the directory `testdata`.
|
||||
|
||||
## Further help
|
||||
|
||||
Feel free to [ask](https://github.com/krateng/maloja/discussions) if you need some help!
|
|
@ -0,0 +1,11 @@
|
|||
Если вы обнаружили этот репозиторий как часть GitHub Arctic Code Vault, я хотел бы искренне извиниться за то, что вы посвятили свой взгляд этому ужасному коду.
|
||||
|
||||
如果您已将该存储库作为GitHub Arctic Code Vault的一部分发现,我谨诚挚地道歉,因为您将目光投向了这一可怕的代码。
|
||||
|
||||
Hoc si repositum partem GitHub arcticum Codicis Buy nudatus, subdens excusare velim simpliciter gravissimum intueri codice.
|
||||
|
||||
Εάν έχετε αποκαλύψει αυτό το αποθετήριο ως μέρος του GitHub Arctic Code Vault, θα ήθελα ειλικρινά να ζητήσω συγνώμη για την υποβολή των ματιών σας σε αυτόν τον τρομερό κώδικα.
|
||||
|
||||
Եթե դուք հայտնաբերել եք այս պահեստը որպես GitHub Arctic Code Vault- ի մաս, ապա ես կցանկանայի անկեղծորեն ներողություն խնդրել ձեր աչքերը այս սարսափելի կոդին ենթարկելու համար:
|
||||
|
||||
If you have uncovered this repository as part of the GitHub Arctic Code Vault, I would like to sincerely apologize for subjecting your eyes to this terrible code.
|
209
README.md
209
README.md
|
@ -1,97 +1,192 @@
|
|||
# Maloja
|
||||
|
||||
[![](https://img.shields.io/github/v/tag/krateng/maloja?label=GitHub&style=for-the-badge&logo=github&logoColor=white)](https://github.com/krateng/maloja)
|
||||
[![](https://img.shields.io/pypi/v/malojaserver?label=PyPI&style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/malojaserver/)
|
||||
[![](https://img.shields.io/docker/v/krateng/maloja?label=Dockerhub&style=for-the-badge&logo=docker&logoColor=white)](https://hub.docker.com/r/krateng/maloja)
|
||||
|
||||
[![](https://img.shields.io/pypi/l/malojaserver?style=for-the-badge)](https://github.com/krateng/maloja/blob/master/LICENSE)
|
||||
[![](https://img.shields.io/codeclimate/maintainability/krateng/maloja?style=for-the-badge)](https://codeclimate.com/github/krateng/maloja)
|
||||
|
||||
Simple self-hosted music scrobble database to create personal listening statistics. No recommendations, no social network, no nonsense.
|
||||
|
||||
You can check [my own Maloja page](https://maloja.krateng.ch) to see what it looks like.
|
||||
![screenshot](https://raw.githubusercontent.com/krateng/maloja/master/screenshot.png)
|
||||
|
||||
## Why not Last.fm / Libre.fm / GNU FM?
|
||||
You can check [my own Maloja page](https://maloja.krateng.ch) as an example instance.
|
||||
|
||||
Maloja is **self-hosted**. You will always be able to access your data in an easily-parseable format. Your library is not synced with any public or official music database, so you can **follow your own tagging schema** or even **group associated artists together** in your charts.
|
||||
|
||||
Maloja also gets **rid of all the extra stuff**: social networking, radios, recommendations, etc. It only keeps track of your listening history and lets you analyze it.
|
||||
## Table of Contents
|
||||
* [Features](#features)
|
||||
* [How to install](#how-to-install)
|
||||
* [Requirements](#requirements)
|
||||
* [PyPI](#pypi)
|
||||
* [From Source](#from-source)
|
||||
* [Docker / Podman](#docker--podman)
|
||||
* [Extras](#extras)
|
||||
* [How to use](#how-to-use)
|
||||
* [Basic control](#basic-control)
|
||||
* [Data](#data)
|
||||
* [Customization](#customization)
|
||||
* [How to scrobble](#how-to-scrobble)
|
||||
* [How to extend](#how-to-extend)
|
||||
|
||||
Maloja's database has one big advantage: It supports **multiple artists per track**. This means artists who are often just "featuring" in the track title get a place in your charts, and **collaborations between several artists finally get credited to all participants**. This allows you to get an actual idea of your artist preferences over time.
|
||||
## Features
|
||||
|
||||
Also neat: You can use your **custom artist or track images**.
|
||||
* **Self-hosted**: You will always be able to access your data in an easily-parseable format. Your library is not synced with any public or official music database, so you can follow your own tagging schema.
|
||||
* **Associated Artists**: Compare different artists' popularity in your listening habits including subunits, collaboration projects or solo performances by their members. Change these associations at any time without losing any information.
|
||||
* **Multi-Artist Tracks**: Some artists often collaborate with others or are listed under "featuring" in the track title. Instead of tracking each combination of artists, each individual artist competes in your charts.
|
||||
* **Custom Images**: Don't rely on the community to select the best pictures for your favorite artists. Upload your own so that your start page looks like you want it to look.
|
||||
* **Proxy Scrobble**: No need to fully commit or set up every client twice - you can configure your Maloja server to forward your scrobbles to other services.
|
||||
* **Standard-compliant API**: Use existing, mature apps or extensions to scrobble to your Maloja server.
|
||||
* **Manual Scrobbling**: Listening to vinyl or elevator background music? Simply submit a scrobble with the web interface.
|
||||
* **Keep it Simple**: Unlike Last.fm and similar alternatives, Maloja doesn't have social networking, radios, recommendations or any other gimmicks. It's a tool to keep track of your listening habits over time - and nothing more.
|
||||
|
||||
## Requirements
|
||||
|
||||
* [python3](https://www.python.org/) - [GitHub](https://github.com/python/cpython)
|
||||
* [bottle.py](https://bottlepy.org/) - [GitHub](https://github.com/bottlepy/bottle)
|
||||
* [waitress](https://docs.pylonsproject.org/projects/waitress/) - [GitHub](https://github.com/Pylons/waitress)
|
||||
* [doreah](https://pypi.org/project/doreah/) - [GitHub](https://github.com/krateng/doreah) (at least Version 0.9.1)
|
||||
* [nimrodel](https://pypi.org/project/nimrodel/) - [GitHub](https://github.com/krateng/nimrodel) (at least Version 0.4.9)
|
||||
* [setproctitle](https://pypi.org/project/setproctitle/) - [GitHub](https://github.com/dvarrazzo/py-setproctitle)
|
||||
* If you'd like to display images, you will need API keys for [Last.fm](https://www.last.fm/api/account/create) and [Fanart.tv](https://fanart.tv/get-an-api-key/). These are free of charge!
|
||||
|
||||
## How to install
|
||||
|
||||
1) Either install Maloja with the [debian package](https://github.com/krateng/maloja/raw/master/packages/maloja.deb), or download the repository to some arbitrary location. If you pick the manual installation, every command needs to be executed from the Maloja directory and led with `./`. You can also only download the file `maloja` instead of the whole repository and fetch the rest with
|
||||
### Requirements
|
||||
|
||||
./maloja install
|
||||
Maloja should run on any x86 or ARM machine that runs Python.
|
||||
|
||||
2) Install required packages with
|
||||
I can support you with issues best if you use **Alpine Linux**.
|
||||
|
||||
pip3 install -r requirements.txt
|
||||
Your CPU should have a single core passmark score of at the very least 1500. 500 MB RAM should give you a decent experience, but performance will benefit greatly from up to 2 GB.
|
||||
|
||||
3) Start the server with
|
||||
### PyPI
|
||||
|
||||
maloja start
|
||||
You can install Maloja with
|
||||
|
||||
```console
|
||||
pip install malojaserver
|
||||
```
|
||||
|
||||
To make sure all dependencies are installed, you can also use one of the included scripts in the `install` folder.
|
||||
|
||||
### From Source
|
||||
|
||||
Clone this repository and enter the directory with
|
||||
|
||||
```console
|
||||
git clone https://github.com/krateng/maloja
|
||||
cd maloja
|
||||
```
|
||||
|
||||
Then install all the requirements and build the package, e.g.:
|
||||
|
||||
```console
|
||||
sh ./install/install_dependencies_alpine.sh
|
||||
pip install -r requirements.txt
|
||||
pip install .
|
||||
```
|
||||
|
||||
### Docker / Podman
|
||||
|
||||
Pull the [latest image](https://hub.docker.com/r/krateng/maloja) or check out the repository and use the included Containerfile.
|
||||
|
||||
Of note are these settings which should be passed as environmental variables to the container:
|
||||
|
||||
* `MALOJA_DATA_DIRECTORY` -- Set the directory in the container where configuration folders/files should be located
|
||||
* Mount a [volume](https://docs.docker.com/engine/reference/builder/#volume) to the specified directory to access these files outside the container (and to make them persistent)
|
||||
* `MALOJA_FORCE_PASSWORD` -- Set an admin password for maloja
|
||||
|
||||
You must publish a port on your host machine to bind to the container's web port (default 42010). The container uses IPv4 per default.
|
||||
|
||||
An example of a minimum run configuration to access maloja via `localhost:42010`:
|
||||
|
||||
```console
|
||||
docker run -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
||||
```
|
||||
|
||||
#### Linux Host
|
||||
|
||||
**NOTE:** If you are using [rootless containers with Podman](https://developers.redhat.com/blog/2020/09/25/rootless-containers-with-podman-the-basics#why_podman_) this DOES NOT apply to you.
|
||||
|
||||
If you are running Docker on a **Linux Host** you should specify `user:group` ids of the user who owns the folder on the host machine bound to `MALOJA_DATA_DIRECTORY` in order to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
|
||||
|
||||
To get the UID and GID for the current user run these commands from a terminal:
|
||||
|
||||
* `id -u` -- prints UID (EX `1000`)
|
||||
* `id -g` -- prints GID (EX `1001`)
|
||||
|
||||
The modified run command with these variables would look like:
|
||||
|
||||
```console
|
||||
docker run -e PUID=1000 -e PGID=1001 -p 42010:42010 -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
||||
```
|
||||
|
||||
### Extras
|
||||
|
||||
* If you'd like to display images, you will need API keys for [Last.fm](https://www.last.fm/api/account/create) and [Spotify](https://developer.spotify.com/dashboard/applications). These are free of charge!
|
||||
|
||||
* Put your server behind a reverse proxy for SSL encryption. Make sure that you're proxying to the IPv6 or IPv4 address according to your settings.
|
||||
|
||||
* You can set up a cronjob to start your server on system boot, and potentially restart it on a regular basis:
|
||||
|
||||
```
|
||||
@reboot sleep 15 && maloja start
|
||||
42 0 7 * * maloja restart
|
||||
```
|
||||
|
||||
4) (Recommended) Put your server behind a reverse proxy for SSL encryption.
|
||||
|
||||
## How to use
|
||||
|
||||
If you didn't install Maloja from the package (and therefore don't have it in `/opt/maloja`), every command needs to be executed from the Maloja directory and led with `./`. Otherwise, all commands work in any location and without the prefix.
|
||||
### Basic control
|
||||
|
||||
Start and stop the server in the background with
|
||||
|
||||
```console
|
||||
maloja start
|
||||
maloja stop
|
||||
maloja restart
|
||||
```
|
||||
|
||||
If you need to run the server in the foreground, use
|
||||
|
||||
```console
|
||||
maloja run
|
||||
```
|
||||
|
||||
|
||||
1) If you would like to import all your previous last.fm scrobbles, use [benfoxall's website](https://benjaminbenben.com/lastfm-to-csv/) ([GitHub page](https://github.com/benfoxall/lastfm-to-csv)). Use the command
|
||||
### Data
|
||||
|
||||
maloja import *filename*
|
||||
If you would like to import your previous scrobbles, use the command `maloja import *filename*`. This works on:
|
||||
|
||||
to import the downloaded file into Maloja.
|
||||
* a Last.fm export generated by [benfoxall's website](https://benjaminbenben.com/lastfm-to-csv/) ([GitHub page](https://github.com/benfoxall/lastfm-to-csv))
|
||||
* an official [Spotify data export file](https://www.spotify.com/us/account/privacy/)
|
||||
* an official [ListenBrainz export file](https://listenbrainz.org/profile/export/)
|
||||
* the export of another Maloja instance
|
||||
|
||||
2) You can interact with the server at any time with the commands
|
||||
⚠️ Never import your data while maloja is running. When you need to do import inside docker container start it in shell mode instead and perform import before starting the container as mentioned above.
|
||||
|
||||
maloja stop
|
||||
maloja restart
|
||||
maloja start
|
||||
maloja update
|
||||
```console
|
||||
docker run -it --entrypoint sh -v $PWD/malojadata:/mljdata -e MALOJA_DATA_DIRECTORY=/mljdata krateng/maloja
|
||||
cd /mljdata
|
||||
maloja import my_last_fm_export.csv
|
||||
```
|
||||
|
||||
The `update` command will always fetch the latest version, while packages are only offered for release versions.
|
||||
---
|
||||
|
||||
3) Various folders have `.info` files with more information on how to use their associated features.
|
||||
To backup your data, run `maloja backup`, optional with `--include_images`.
|
||||
|
||||
### Customization
|
||||
|
||||
* Have a look at the [available settings](settings.md) and specifiy your choices in `/etc/maloja/settings.ini`. You can also set each of these settings as an environment variable with the prefix `MALOJA_` (e.g. `MALOJA_SKIP_SETUP`).
|
||||
|
||||
* If you have activated admin mode in your web interface, you can upload custom images for artists or tracks by simply dragging them onto the existing image on the artist or track page. You can also manage custom images directly in the file system - consult `images.info` in the `/var/lib/maloja/images` folder.
|
||||
|
||||
* To specify custom rules, consult the `rules.info` file in `/etc/maloja/rules`. You can also apply some predefined rules on the `/admin_setup` page of your server.
|
||||
|
||||
4) If you'd like to implement anything on top of Maloja, visit `/api_explorer`.
|
||||
|
||||
## How to scrobble
|
||||
|
||||
### Native API
|
||||
You can set up any amount of API keys in the file `apikeys.yml` in your configuration folder (or via the web interface). It is recommended to define a different API key for every scrobbler you use.
|
||||
|
||||
If you use Plex Web or Youtube Music on Chromium, you can use the included extension (also available on the [Chrome Web Store](https://chrome.google.com/webstore/detail/maloja-scrobbler/cfnbifdmgbnaalphodcbandoopgbfeeh)). Make sure to enter the random key Maloja generates on first startup in the extension settings.
|
||||
Some scrobbler clients support Maloja's native API. You can also use any scrobbler that allows you to set a custom Listenbrainz or GNUFM server. See [API.md](API.md) for details.
|
||||
|
||||
If you want to implement your own method of scrobbling, it's very simple: You only need one POST request to `/api/newscrobble` with the keys `artist`, `title` and `key` - either as from-data or json.
|
||||
If you're the maintainer of a music player or server and would like to implement native Maloja scrobbling, feel free to reach out!
|
||||
|
||||
### Standard-compliant API
|
||||
If you can't automatically scrobble your music, you can always do it manually on the `/admin_manual` page of your Maloja server.
|
||||
|
||||
You can use any third-party scrobbler that supports the audioscrobbler (GNUFM) or the ListenBrainz protocol. This is still very experimental, but give it a try with these settings:
|
||||
|
||||
GNU FM |
|
||||
------ | ---------
|
||||
Gnukebox URL | Your Maloja URL followed by `/api/s/audioscrobbler`
|
||||
Username | Any name, doesn't matter
|
||||
Password | Any of your API keys
|
||||
## How to extend
|
||||
|
||||
ListenBrainz |
|
||||
------ | ---------
|
||||
API URL | Your Maloja URL followed by `/api/s/listenbrainz`
|
||||
Username | Any name, doesn't matter
|
||||
Auth Token | Any of your API keys
|
||||
|
||||
These are tested with the Pano Scrobbler and the Simple Last.fm Scrobbler for Android. I'm thankful for any feedback whether other scrobblers work!
|
||||
|
||||
It is recommended to define a different API key for every scrobbler you use in `clients/authenticated_machines.tsv` in your Maloja folder.
|
||||
|
||||
### Manual
|
||||
|
||||
If you can't automatically scrobble your music, you can always do it manually on the `/manual` page of your Maloja server.
|
||||
If you'd like to implement anything on top of Maloja, visit `/api_explorer`.
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
screenshot.png
|
||||
tile.png
|
||||
!*.sh
|
Binary file not shown.
|
@ -1,5 +1,4 @@
|
|||
|
||||
|
||||
chrome.tabs.onUpdated.addListener(onTabUpdated);
|
||||
chrome.tabs.onRemoved.addListener(onTabRemoved);
|
||||
//chrome.tabs.onActivated.addListener(onTabChanged);
|
||||
|
@ -7,8 +6,13 @@ chrome.runtime.onMessage.addListener(onInternalMessage);
|
|||
|
||||
tabManagers = {}
|
||||
|
||||
|
||||
const ALWAYS_SCROBBLE_SECONDS = 60*3;
|
||||
// Longer songs are always scrobbled when playing at least 2 minutes
|
||||
|
||||
pages = {
|
||||
"Plex Web":{
|
||||
"plex":{
|
||||
"name":"Plex",
|
||||
"patterns":[
|
||||
"https://app.plex.tv",
|
||||
"http://app.plex.tv",
|
||||
|
@ -17,17 +21,41 @@ pages = {
|
|||
],
|
||||
"script":"plex.js"
|
||||
},
|
||||
"YouTube Music":{
|
||||
"ytmusic":{
|
||||
"name":"YouTube Music",
|
||||
"patterns":[
|
||||
"https://music.youtube.com"
|
||||
],
|
||||
"script":"ytmusic.js"
|
||||
},
|
||||
"Spotify Web":{
|
||||
"spotify":{
|
||||
"name":"Spotify",
|
||||
"patterns":[
|
||||
"https://open.spotify.com"
|
||||
],
|
||||
"script":"spotify.js"
|
||||
},
|
||||
"bandcamp":{
|
||||
"name":"Bandcamp",
|
||||
"patterns":[
|
||||
"bandcamp.com"
|
||||
],
|
||||
"script":"bandcamp.js"
|
||||
},
|
||||
"soundcloud":{
|
||||
"name":"Soundcloud",
|
||||
"patterns":[
|
||||
"https://soundcloud.com"
|
||||
],
|
||||
"script":"soundcloud.js"
|
||||
},
|
||||
"navidrome":{
|
||||
"name":"Navidrome",
|
||||
"patterns":[
|
||||
"https://navidrome.",
|
||||
"http://navidrome."
|
||||
],
|
||||
"script":"navidrome.js"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,10 +79,17 @@ function onTabUpdated(tabId, changeInfo, tab) {
|
|||
patterns = pages[page]["patterns"];
|
||||
//console.log("Page was managed by a " + page + " manager")
|
||||
for (var i=0;i<patterns.length;i++) {
|
||||
if (tab.url.startsWith(patterns[i])) {
|
||||
if (tab.url.includes(patterns[i])) {
|
||||
//console.log("Still on same page!")
|
||||
tabManagers[tabId].update();
|
||||
|
||||
// check if the setting for this page is still active
|
||||
chrome.storage.local.get(["service_active_" + page],function(result){
|
||||
if (!result["service_active_" + page]) {
|
||||
delete tabManagers[tabId];
|
||||
}
|
||||
});
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -67,14 +102,22 @@ function onTabUpdated(tabId, changeInfo, tab) {
|
|||
if (pages.hasOwnProperty(key)) {
|
||||
patterns = pages[key]["patterns"];
|
||||
for (var i=0;i<patterns.length;i++) {
|
||||
if (tab.url.startsWith(patterns[i])) {
|
||||
console.log("New page on tab " + tabId + " will be handled by new " + key + " manager!");
|
||||
tabManagers[tabId] = new Controller(tabId,key);
|
||||
updateTabNum();
|
||||
return
|
||||
//chrome.tabs.executeScript(tab.id,{"file":"sitescripts/" + pages[key]["script"]})
|
||||
if (tab.url.includes(patterns[i])) {
|
||||
|
||||
// check if we even like that page
|
||||
chrome.storage.local.get(["service_active_" + key],function(result){
|
||||
if (result["service_active_" + key]) {
|
||||
console.log("New page on tab " + tabId + " will be handled by new " + key + " manager!");
|
||||
tabManagers[tabId] = new Controller(tabId,key);
|
||||
updateTabNum();
|
||||
//chrome.tabs.executeScript(tab.id,{"file":"sitescripts/" + pages[key]["script"]})
|
||||
}
|
||||
else {
|
||||
console.log("New page on tab " + tabId + " is " + key + ", not enabled!");
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,10 +148,10 @@ function onInternalMessage(request,sender) {
|
|||
for (tabId in tabManagers) {
|
||||
manager = tabManagers[tabId]
|
||||
if (manager.currentlyPlaying) {
|
||||
answer.push([manager.page,manager.currentArtist,manager.currentTitle]);
|
||||
answer.push([pages[manager.page]['name'],manager.currentArtist,manager.currentTitle]);
|
||||
}
|
||||
else {
|
||||
answer.push([manager.page,null]);
|
||||
answer.push([pages[manager.page]['name'],null]);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -166,8 +209,13 @@ class Controller {
|
|||
actuallyupdate() {
|
||||
this.messageID++;
|
||||
//console.log("Update! Our page is " + this.page + ", our tab id " + this.tabId)
|
||||
chrome.tabs.executeScript(this.tabId,{"file":"sites/" + pages[this.page]["script"]});
|
||||
chrome.tabs.executeScript(this.tabId,{"file":"sitescript.js"});
|
||||
try {
|
||||
chrome.tabs.executeScript(this.tabId,{"file":"sites/" + pages[this.page]["script"]});
|
||||
chrome.tabs.executeScript(this.tabId,{"file":"sitescript.js"});
|
||||
}
|
||||
catch (e) {
|
||||
console.log("Could not run site script. Tab probably closed or something idk.")
|
||||
}
|
||||
|
||||
this.alreadyQueued = false;
|
||||
}
|
||||
|
@ -190,6 +238,14 @@ class Controller {
|
|||
}
|
||||
}
|
||||
|
||||
backlog_scrobble() {
|
||||
while (this.alreadyPlayed > this.currentLength) {
|
||||
this.alreadyPlayed = this.alreadyPlayed - this.currentLength
|
||||
var secondsago = this.alreadyPlayed
|
||||
scrobble(this.currentArtist,this.currentTitle,this.currentLength,secondsago)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -199,12 +255,8 @@ class Controller {
|
|||
if (artist == this.currentArtist && title == this.currentTitle && !this.currentlyPlaying) {
|
||||
console.log("Resuming playback of " + this.currentTitle)
|
||||
|
||||
// Already played full song
|
||||
while (this.alreadyPlayed > this.currentLength) {
|
||||
this.alreadyPlayed = this.alreadyPlayed - this.currentLength
|
||||
var secondsago = this.alreadyPlayed
|
||||
scrobble(this.currentArtist,this.currentTitle,this.currentLength,secondsago)
|
||||
}
|
||||
// Already played full song?
|
||||
this.backlog_scrobble()
|
||||
|
||||
this.setUpdate()
|
||||
this.currentlyPlaying = true
|
||||
|
@ -246,12 +298,8 @@ class Controller {
|
|||
}
|
||||
|
||||
|
||||
// Already played full song
|
||||
while (this.alreadyPlayed > this.currentLength) {
|
||||
this.alreadyPlayed = this.alreadyPlayed - this.currentLength
|
||||
var secondsago = this.alreadyPlayed
|
||||
scrobble(this.currentArtist,this.currentTitle,this.currentLength,secondsago)
|
||||
}
|
||||
// Already played full song?
|
||||
this.backlog_scrobble()
|
||||
|
||||
this.currentlyPlaying = false
|
||||
|
||||
|
@ -259,7 +307,7 @@ class Controller {
|
|||
|
||||
//ONLY CASE 2: Playback ended
|
||||
if (artist != this.currentArtist || title != this.currentTitle) {
|
||||
if (this.alreadyPlayed > this.currentLength / 2) {
|
||||
if ((this.alreadyPlayed > this.currentLength / 2) || (this.alreadyPlayed > ALWAYS_SCROBBLE_SECONDS)) {
|
||||
scrobble(this.currentArtist,this.currentTitle,this.alreadyPlayed)
|
||||
this.alreadyPlayed = 0
|
||||
}
|
||||
|
@ -286,21 +334,25 @@ class Controller {
|
|||
|
||||
function scrobble(artist,title,seconds,secondsago=0) {
|
||||
console.log("Scrobbling " + artist + " - " + title + "; " + seconds + " seconds playtime, " + secondsago + " seconds ago")
|
||||
var artiststring = encodeURIComponent(artist)
|
||||
var titlestring = encodeURIComponent(title)
|
||||
var d = new Date()
|
||||
var time = Math.floor(d.getTime()/1000) - secondsago
|
||||
//console.log("Time: " + time)
|
||||
var requestbody = "artist=" + artiststring + "&title=" + titlestring + "&duration=" + seconds + "&time=" + time
|
||||
chrome.storage.local.get("apikey",function(result) {
|
||||
APIKEY = result["apikey"]
|
||||
chrome.storage.local.get("serverurl",function(result) {
|
||||
URL = result["serverurl"]
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST",URL + "/api/newscrobble",true);
|
||||
xhttp.send(requestbody + "&key=" + APIKEY)
|
||||
//console.log("Sent: " + requestbody)
|
||||
});
|
||||
var payload = {
|
||||
"artist":artist,
|
||||
"title":title,
|
||||
"duration":seconds,
|
||||
"time":time
|
||||
}
|
||||
|
||||
chrome.storage.local.get(["serverurl","apikey"],function(result) {
|
||||
payload["key"] = result["apikey"];
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.open("POST",result["serverurl"] + "/apis/mlj_1/newscrobble",true);
|
||||
xhttp.setRequestHeader("Content-Type", "application/json");
|
||||
//xhttp.send(requestbody + "&key=" + APIKEY)
|
||||
var body = JSON.stringify(payload);
|
||||
xhttp.send(body)
|
||||
//console.log("Sent: " + body)
|
||||
});
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
|
@ -1,15 +1,13 @@
|
|||
{
|
||||
"name": "Maloja Scrobbler",
|
||||
"version": "1.3",
|
||||
"version": "1.13",
|
||||
"description": "Scrobbles tracks from various sites to your Maloja server",
|
||||
"manifest_version": 2,
|
||||
"permissions": ["activeTab",
|
||||
"declarativeContent",
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"storage",
|
||||
"http://*/",
|
||||
"https://*/",
|
||||
"<all_urls>"
|
||||
"https://*/"
|
||||
],
|
||||
"background":
|
||||
{
|
||||
|
@ -28,7 +26,7 @@
|
|||
"48":"icon48.png"
|
||||
},
|
||||
"default_popup": "settings.html",
|
||||
"default_title": "Settings"
|
||||
"default_title": "Maloja Scrobbler"
|
||||
},
|
||||
"icons":
|
||||
{
|
|
@ -14,7 +14,7 @@
|
|||
color:beige;
|
||||
font-family:'Ubuntu';
|
||||
}
|
||||
input {
|
||||
input[type=text] {
|
||||
width:270px;
|
||||
font-family:'Ubuntu';
|
||||
outline:none;
|
||||
|
@ -29,14 +29,18 @@
|
|||
<body>
|
||||
<div id="wat">
|
||||
<span id="checkmark_url"></span> <span>Server:</span><br />
|
||||
<input type="text" id="serverurl" value="http://localhost:42010" />
|
||||
<input type="text" id="serverurl" />
|
||||
<br /><br />
|
||||
<span id="checkmark_key"></span> <span>API key:</span><br />
|
||||
<input type="text" id="apikey" />
|
||||
<br/><br/>
|
||||
<hr/>
|
||||
<span>Tabs:</span>
|
||||
<list id="playinglist">
|
||||
</list>
|
||||
<hr/>
|
||||
<span>Services:</span>
|
||||
<list id="sitelist">
|
||||
</list>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,7 +1,44 @@
|
|||
// duplicate this info for now, don't know if there is a better way than sending messages
|
||||
var pages = {
|
||||
"plex":"Plex",
|
||||
"ytmusic":"YouTube Music",
|
||||
"spotify":"Spotify",
|
||||
"bandcamp":"Bandcamp",
|
||||
"soundcloud":"Soundcloud",
|
||||
"navidrome":"Navidrome"
|
||||
}
|
||||
|
||||
var config_defaults = {
|
||||
serverurl:"http://localhost:42010",
|
||||
apikey:"BlackPinkInYourArea"
|
||||
}
|
||||
|
||||
for (var key in pages) {
|
||||
config_defaults["service_active_" + key] = true;
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded",function() {
|
||||
document.getElementById("serverurl").addEventListener("input",updateServer);
|
||||
document.getElementById("apikey").addEventListener("input",updateAPIKey);
|
||||
|
||||
var sitelist = document.getElementById("sitelist");
|
||||
|
||||
|
||||
for (var identifier in pages) {
|
||||
sitelist.append(document.createElement('br'));
|
||||
var checkbox = document.createElement('input');
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.id = "service_active_" + identifier;
|
||||
var label = document.createElement('label');
|
||||
label.for = checkbox.id;
|
||||
label.textContent = pages[identifier];
|
||||
sitelist.appendChild(checkbox);
|
||||
sitelist.appendChild(label);
|
||||
|
||||
checkbox.addEventListener("change",toggleSite);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
document.getElementById("serverurl").addEventListener("change",checkServer);
|
||||
document.getElementById("apikey").addEventListener("change",checkServer);
|
||||
|
@ -9,18 +46,29 @@ document.addEventListener("DOMContentLoaded",function() {
|
|||
document.getElementById("serverurl").addEventListener("focusout",checkServer);
|
||||
document.getElementById("apikey").addEventListener("focusout",checkServer);
|
||||
|
||||
document.getElementById("serverurl").addEventListener("input",saveServer);
|
||||
document.getElementById("apikey").addEventListener("input",saveServer);
|
||||
|
||||
|
||||
chrome.runtime.onMessage.addListener(onInternalMessage);
|
||||
|
||||
chrome.storage.local.get(config_defaults,function(result){
|
||||
console.log(result);
|
||||
for (var key in result) {
|
||||
|
||||
chrome.storage.local.get({"serverurl":"http://localhost:42010"},function(result) {
|
||||
document.getElementById("serverurl").value = result["serverurl"]
|
||||
checkServerMaybe()
|
||||
});
|
||||
chrome.storage.local.get({"apikey":"BlackPinkInYourArea"},function(result) {
|
||||
document.getElementById("apikey").value = result["apikey"]
|
||||
checkServerMaybe()
|
||||
});
|
||||
// booleans
|
||||
if (result[key] == true || result[key] == false) {
|
||||
document.getElementById(key).checked = result[key];
|
||||
}
|
||||
|
||||
// text
|
||||
else{
|
||||
document.getElementById(key).value = result[key];
|
||||
}
|
||||
|
||||
}
|
||||
checkServer();
|
||||
})
|
||||
|
||||
chrome.runtime.sendMessage({"type":"query"})
|
||||
|
||||
|
@ -28,18 +76,12 @@ document.addEventListener("DOMContentLoaded",function() {
|
|||
|
||||
});
|
||||
|
||||
|
||||
//this makes sure only the second call actually makes a request (the first request is pointless
|
||||
//when the other element isn't filled yet and might actually overwrite the correct result because
|
||||
//of a race condition)
|
||||
var done = 0
|
||||
function checkServerMaybe() {
|
||||
done++;
|
||||
if (done == 2) {
|
||||
checkServer()
|
||||
}
|
||||
function toggleSite(evt) {
|
||||
var element = evt.target;
|
||||
chrome.storage.local.set({ [element.id]: element.checked });
|
||||
}
|
||||
|
||||
|
||||
function onInternalMessage(request,sender) {
|
||||
if (request.type == "response") {
|
||||
players = request.content
|
||||
|
@ -58,17 +100,11 @@ function onInternalMessage(request,sender) {
|
|||
|
||||
|
||||
|
||||
function updateServer() {
|
||||
|
||||
text = document.getElementById("serverurl").value
|
||||
|
||||
|
||||
chrome.storage.local.set({"serverurl":text})
|
||||
}
|
||||
|
||||
function updateAPIKey() {
|
||||
text = document.getElementById("apikey").value
|
||||
chrome.storage.local.set({"apikey":text})
|
||||
function saveServer() {
|
||||
for (var key of ["serverurl","apikey"]) {
|
||||
var value = document.getElementById(key).value;
|
||||
chrome.storage.local.set({ [key]: value });
|
||||
}
|
||||
}
|
||||
|
||||
function checkServer() {
|
||||
|
@ -91,7 +127,7 @@ function checkServer() {
|
|||
|
||||
function createCheckmarks() {
|
||||
if (this.readyState == 4) {
|
||||
if ((this.status == 204) || (this.status == 205)) {
|
||||
if ((this.status >= 200) && (this.status < 300)) {
|
||||
//document.getElementById("checkmark_url").innerHTML = "✔️"
|
||||
//document.getElementById("checkmark_key").innerHTML = "✔️"
|
||||
document.getElementById("serverurl").style.backgroundColor = "lawngreen"
|
|
@ -0,0 +1,15 @@
|
|||
maloja_scrobbler_selector_playbar = "//div[contains(@class,'trackView')]"
|
||||
|
||||
|
||||
maloja_scrobbler_selector_metadata = "."
|
||||
// need to select everything as bar / metadata block because artist isn't shown in the inline player
|
||||
|
||||
maloja_scrobbler_selector_title = ".//span[@class='title']/text()"
|
||||
maloja_scrobbler_selector_artist = ".//span[contains(@itemprop,'byArtist')]/a/text()"
|
||||
maloja_scrobbler_selector_duration = ".//span[@class='time_total']/text()"
|
||||
|
||||
|
||||
maloja_scrobbler_selector_control = ".//td[@class='play_cell']/a[@role='button']/div[contains(@class,'playbutton')]/@class"
|
||||
|
||||
maloja_scrobbler_label_playing = "playbutton playing"
|
||||
maloja_scrobbler_label_paused = "playbutton"
|
|
@ -0,0 +1,14 @@
|
|||
maloja_scrobbler_selector_playbar = "//div[contains(@class,'music-player-panel')]"
|
||||
|
||||
|
||||
maloja_scrobbler_selector_metadata = ".//span[contains(@class,'audio-title')]"
|
||||
|
||||
maloja_scrobbler_selector_title = ".//span[contains(@class,'songTitle')]/text()"
|
||||
maloja_scrobbler_selector_artist = ".//span[contains(@class,'songArtist')]/text()"
|
||||
maloja_scrobbler_selector_duration = ".//span[contains(@class,'duration')]/text()"
|
||||
|
||||
|
||||
maloja_scrobbler_selector_control = ".//span[contains(@class,'group play-btn')]/@title"
|
||||
|
||||
maloja_scrobbler_label_playing = "Click to pause"
|
||||
maloja_scrobbler_label_paused = "Click to play"
|
|
@ -3,9 +3,9 @@ maloja_scrobbler_selector_playbar = "//div[contains(@class,'PlayerControls')]"
|
|||
|
||||
maloja_scrobbler_selector_metadata = ".//div[contains(@class,'PlayerControlsMetadata-container')]"
|
||||
|
||||
maloja_scrobbler_selector_title = ".//a[@data-qa-id='metadataTitleLink']/@title"
|
||||
maloja_scrobbler_selector_title = ".//a[@data-testid='metadataTitleLink']/@title"
|
||||
maloja_scrobbler_selector_artist = ".//span[contains(@class,'MetadataPosterTitle-title')]/a[1]/@title"
|
||||
maloja_scrobbler_selector_duration = ".//button[@data-qa-id='mediaDuration']/text()[3]"
|
||||
maloja_scrobbler_selector_duration = ".//button[@data-testid='mediaDuration']/text()[3]"
|
||||
|
||||
|
||||
maloja_scrobbler_selector_control = ".//div[contains(@class,'PlayerControls-buttonGroupCenter')]/button[2]/@title"
|
|
@ -0,0 +1,14 @@
|
|||
maloja_scrobbler_selector_playbar = "//div[contains(@class,'playControls')]"
|
||||
|
||||
|
||||
maloja_scrobbler_selector_metadata = ".//div[contains(@class,'playControls__soundBadge')]//div[contains(@class,'playbackSoundBadge__titleContextContainer')]"
|
||||
|
||||
maloja_scrobbler_selector_title = ".//div/a/@title"
|
||||
maloja_scrobbler_selector_artist = ".//a/text()"
|
||||
maloja_scrobbler_selector_duration = ".//div[contains(@class,'playbackTimeline__duration')]//span[@aria-hidden='true']/text()"
|
||||
|
||||
|
||||
maloja_scrobbler_selector_control = ".//button[contains(@class,'playControl')]/@title"
|
||||
|
||||
maloja_scrobbler_label_playing = "Pause current"
|
||||
maloja_scrobbler_label_paused = "Play current"
|
|
@ -0,0 +1,12 @@
|
|||
maloja_scrobbler_selector_playbar = "//footer[@data-testid='now-playing-bar']"
|
||||
|
||||
|
||||
maloja_scrobbler_selector_metadata = ".//div[@data-testid='now-playing-widget']"
|
||||
|
||||
maloja_scrobbler_selector_title = ".//a[@data-testid='context-item-link']/text()"
|
||||
maloja_scrobbler_selector_artists = ".//a[contains(@href,'/artist/')]"
|
||||
maloja_scrobbler_selector_artist = "./text()"
|
||||
maloja_scrobbler_selector_duration = ".//div[@data-testid='playback-duration']/text()"
|
||||
|
||||
|
||||
maloja_scrobbler_selector_control = ".//button[@data-testid='control-button-playpause']/@aria-label"
|
|
@ -32,7 +32,7 @@ Node.prototype.xpath = getxpath;
|
|||
|
||||
bar = document.xpath(maloja_scrobbler_selector_playbar, XPathResult.FIRST_ORDERED_NODE_TYPE);
|
||||
if (bar == null) {
|
||||
console.log("Nothing playing right now!");
|
||||
console.log("[Maloja Scrobbler] Nothing playing right now!");
|
||||
chrome.runtime.sendMessage({type:"stopPlayback",time:Math.floor(Date.now()),artist:"",title:""});
|
||||
}
|
||||
else {
|
||||
|
@ -65,13 +65,25 @@ else {
|
|||
|
||||
|
||||
control = bar.xpath(maloja_scrobbler_selector_control, XPathResult.STRING_TYPE);
|
||||
if (control == "Play") {
|
||||
console.log("Not playing right now");
|
||||
try {
|
||||
label_playing = maloja_scrobbler_label_playing
|
||||
}
|
||||
catch {
|
||||
label_playing = "Pause"
|
||||
}
|
||||
try {
|
||||
label_paused = maloja_scrobbler_label_paused
|
||||
}
|
||||
catch {
|
||||
label_paused = "Play"
|
||||
}
|
||||
if (control == label_paused) {
|
||||
console.log("[Maloja Scrobbler] Not playing right now");
|
||||
chrome.runtime.sendMessage({type:"stopPlayback",time:Math.floor(Date.now()),artist:artist,title:title});
|
||||
//stopPlayback()
|
||||
}
|
||||
else if (control == "Pause") {
|
||||
console.log("Playing " + artist + " - " + title + " (" + durationSeconds + " sec)");
|
||||
else if (control == label_playing) {
|
||||
console.log("[Maloja Scrobbler] Playing " + artist + " - " + title + " (" + durationSeconds + " sec)");
|
||||
chrome.runtime.sendMessage({type:"startPlayback",time:Math.floor(Date.now()),artist:artist,title:title,duration:durationSeconds});
|
||||
//startPlayback(artist,title,durationSeconds)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# maloja-lib
|
||||
|
||||
Library for Python music players to allow users to scrobble to [Maloja](https://github.com/krateng/maloja) servers.
|
||||
|
||||
```
|
||||
from malojalib import MalojaInstance
|
||||
|
||||
instance = MalojaInstance(user_supplied_url,user_supplied_key)
|
||||
|
||||
instance.scrobble(artists=['K/DA','Howard Shore','Blackbeard's Tea Party],title='Grüezi Wohl Frau Stirnimaa')
|
||||
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
import requests
|
||||
|
||||
class MalojaInstance:
|
||||
def __init__(self,base_url,key):
|
||||
self.base_url = base_url
|
||||
self.key = key
|
||||
|
||||
def test(self):
|
||||
url = self.base_url + '/apis/mlj_1/test'
|
||||
response = requests.get(url,{'key':self.key})
|
||||
|
||||
return (response.status_code == 200)
|
||||
|
||||
def scrobble(self,artists,title,timestamp=None,album=None,duration=None):
|
||||
payload = {
|
||||
'key':self.key,
|
||||
'artists':artists,
|
||||
'title':title,
|
||||
'time':timestamp,
|
||||
'album':album,
|
||||
'duration':duration
|
||||
}
|
||||
|
||||
url = self.base_url + '/apis/mlj_1/newscrobble'
|
||||
response = requests.post(url,payload)
|
||||
|
||||
return response.json()
|
|
@ -0,0 +1,27 @@
|
|||
[project]
|
||||
name = "maloja-lib"
|
||||
version = "1.0.0"
|
||||
description = "Utilities to interact with Maloja servers"
|
||||
readme = "./README.md"
|
||||
requires-python = ">=3.6"
|
||||
license = { file="../../LICENSE" }
|
||||
authors = [ { name="Johannes Krattenmacher", email="maloja@dev.krateng.ch" } ]
|
||||
|
||||
urls.repository = "https://github.com/krateng/maloja"
|
||||
urls.documentation = "https://github.com/krateng/maloja"
|
||||
|
||||
keywords = ["scrobbling", "music", "library", "api"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent"
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"requests"
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["flit_core >=3.2,<4"]
|
||||
build-backend = "flit_core.buildapi"
|
|
@ -1,3 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
||||
!*.info
|
201
cleanup.py
201
cleanup.py
|
@ -1,201 +0,0 @@
|
|||
import re
|
||||
import utilities
|
||||
from doreah import tsv
|
||||
|
||||
# need to do this as a class so it can retain loaded settings from file
|
||||
# apparently this is not true
|
||||
# I'm dumb
|
||||
class CleanerAgent:
|
||||
|
||||
def __init__(self):
|
||||
self.updateRules()
|
||||
|
||||
def updateRules(self):
|
||||
raw = tsv.parse_all("rules","string","string","string")
|
||||
self.rules_belongtogether = [b for [a,b,c] in raw if a=="belongtogether"]
|
||||
self.rules_notanartist = [b for [a,b,c] in raw if a=="notanartist"]
|
||||
self.rules_replacetitle = {b.lower():c for [a,b,c] in raw if a=="replacetitle"}
|
||||
self.rules_replaceartist = {b.lower():c for [a,b,c] in raw if a=="replaceartist"}
|
||||
|
||||
# we always need to be able to tell if our current database is made with the current rules
|
||||
self.checksums = utilities.checksumTSV("rules")
|
||||
|
||||
|
||||
|
||||
def fullclean(self,artist,title):
|
||||
artists = self.parseArtists(self.removespecial(artist))
|
||||
title = self.parseTitle(self.removespecial(title))
|
||||
(title,moreartists) = self.parseTitleForArtists(title)
|
||||
artists += moreartists
|
||||
artists = list(set(artists))
|
||||
artists.sort()
|
||||
|
||||
return (artists,title)
|
||||
|
||||
def removespecial(self,s):
|
||||
s = s.replace("\t","").replace("␟","").replace("\n","")
|
||||
s = re.sub(" +"," ",s)
|
||||
return s
|
||||
|
||||
|
||||
# if an artist appears in any created rule, we can assume that artist is meant to exist and be spelled like that
|
||||
def confirmedReal(self,a):
|
||||
confirmed = self.rules_belongtogether + [self.rules_replaceartist[r] for r in self.rules_replaceartist]
|
||||
return (a in confirmed)
|
||||
|
||||
#Delimiters used for extra artists, even when in the title field
|
||||
delimiters_feat = ["ft.","ft","feat.","feat","featuring","Ft.","Ft","Feat.","Feat","Featuring"]
|
||||
#Delimiters in informal artist strings, spaces expected around them
|
||||
delimiters = ["vs.","vs","&"]
|
||||
#Delimiters used specifically to tag multiple artists when only one tag field is available, no spaces used
|
||||
delimiters_formal = ["; ",";","/"]
|
||||
|
||||
def parseArtists(self,a):
|
||||
|
||||
if a.strip() == "":
|
||||
return []
|
||||
|
||||
if a.strip() in self.rules_notanartist:
|
||||
return []
|
||||
|
||||
if " performing " in a.lower():
|
||||
return self.parseArtists(re.split(" [Pp]erforming",a)[0])
|
||||
|
||||
if a.strip() in self.rules_belongtogether:
|
||||
return [a.strip()]
|
||||
if a.strip().lower() in self.rules_replaceartist:
|
||||
return self.rules_replaceartist[a.strip().lower()].split("␟")
|
||||
|
||||
|
||||
|
||||
for d in self.delimiters_feat:
|
||||
if re.match(r"(.*) \(" + d + " (.*)\)",a) is not None:
|
||||
return self.parseArtists(re.sub(r"(.*) \(" + d + " (.*)\)",r"\1",a)) + \
|
||||
self.parseArtists(re.sub(r"(.*) \(" + d + " (.*)\)",r"\2",a))
|
||||
|
||||
for d in self.delimiters_formal:
|
||||
if (d in a):
|
||||
ls = []
|
||||
for i in a.split(d):
|
||||
ls += self.parseArtists(i)
|
||||
return ls
|
||||
|
||||
for d in (self.delimiters_feat + self.delimiters):
|
||||
if ((" " + d + " ") in a):
|
||||
ls = []
|
||||
for i in a.split(" " + d + " "):
|
||||
ls += self.parseArtists(i)
|
||||
return ls
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return [a.strip()]
|
||||
|
||||
def parseTitle(self,t):
|
||||
if t.strip().lower() in self.rules_replacetitle:
|
||||
return self.rules_replacetitle[t.strip().lower()]
|
||||
|
||||
t = t.replace("[","(").replace("]",")")
|
||||
|
||||
t = re.sub(r" \(as made famous by .*?\)","",t)
|
||||
t = re.sub(r" \(originally by .*?\)","",t)
|
||||
t = re.sub(r" \(.*?Remaster.*?\)","",t)
|
||||
|
||||
return t.strip()
|
||||
|
||||
def parseTitleForArtists(self,t):
|
||||
for d in self.delimiters_feat:
|
||||
if re.match(r"(.*) \(" + d + " (.*?)\)",t) is not None:
|
||||
(title,artists) = self.parseTitleForArtists(re.sub(r"(.*) \(" + d + " (.*?)\)",r"\1",t))
|
||||
artists += self.parseArtists(re.sub(r"(.*) \(" + d + " (.*?)\).*",r"\2",t))
|
||||
return (title,artists)
|
||||
if re.match(r"(.*) - " + d + " (.*)",t) is not None:
|
||||
(title,artists) = self.parseTitleForArtists(re.sub(r"(.*) - " + d + " (.*)",r"\1",t))
|
||||
artists += self.parseArtists(re.sub(r"(.*) - " + d + " (.*).*",r"\2",t))
|
||||
return (title,artists)
|
||||
if re.match(r"(.*) " + d + " (.*)",t) is not None:
|
||||
(title,artists) = self.parseTitleForArtists(re.sub(r"(.*) " + d + " (.*)",r"\1",t))
|
||||
artists += self.parseArtists(re.sub(r"(.*) " + d + " (.*).*",r"\2",t))
|
||||
return (title,artists)
|
||||
|
||||
return (t,[])
|
||||
|
||||
|
||||
|
||||
#this is for all the runtime changes (counting Trouble Maker as HyunA for charts etc)
|
||||
class CollectorAgent:
|
||||
|
||||
def __init__(self):
|
||||
self.updateRules()
|
||||
|
||||
# rules_countas dict: real artist -> credited artist
|
||||
# rules_countas_id dict: real artist ID -> credited artist ID
|
||||
# rules_include dict: credited artist -> all real artists
|
||||
|
||||
def updateRules(self):
|
||||
raw = tsv.parse_all("rules","string","string","string")
|
||||
self.rules_countas = {b:c for [a,b,c] in raw if a=="countas"}
|
||||
self.rules_countas_id = {}
|
||||
self.rules_include = {} #Twice the memory, double the performance!
|
||||
# (Yes, we're saving redundant information here, but it's not unelegant if it's within a closed object!)
|
||||
for a in self.rules_countas:
|
||||
self.rules_include[self.rules_countas[a]] = self.rules_include.setdefault(self.rules_countas[a],[]) + [a]
|
||||
|
||||
# this agent needs to be aware of the current id assignment in the main program
|
||||
# unelegant, but the best way i can think of
|
||||
def updateIDs(self,artistlist):
|
||||
self.rules_countas_id = {artistlist.index(a):artistlist.index(self.rules_countas[a]) for a in self.rules_countas if a in artistlist}
|
||||
#self.rules_include_id = {artistlist.index(a):artistlist.index(self.rules_include[a]) for a in self.rules_include}
|
||||
#this needs to take lists into account
|
||||
|
||||
|
||||
# get who is credited for this artist
|
||||
def getCredited(self,artist):
|
||||
if artist in self.rules_countas:
|
||||
return self.rules_countas[artist]
|
||||
if artist in self.rules_countas_id:
|
||||
return self.rules_countas_id[artist]
|
||||
|
||||
else:
|
||||
return artist
|
||||
|
||||
# get all credited artists for the artists given
|
||||
def getCreditedList(self,artists):
|
||||
updatedArtists = []
|
||||
for artist in artists:
|
||||
updatedArtists.append(self.getCredited(artist))
|
||||
return list(set(updatedArtists))
|
||||
|
||||
# get artists who the given artist is given credit for
|
||||
def getAllAssociated(self,artist):
|
||||
return self.rules_include.get(artist,[])
|
||||
|
||||
# this function is there to check for artists that we should include in the
|
||||
# database even though they never have any scrobble.
|
||||
def getAllArtists(self):
|
||||
return list(set([self.rules_countas[a] for a in self.rules_countas]))
|
||||
# artists that count can be nonexisting (counting HyunA as 4Minute even
|
||||
# though 4Minute has never been listened to)
|
||||
# but artists that are counted as someone else are only relevant if they
|
||||
# exist (so we can preemptively declare lots of rules just in case)
|
||||
#return list(set([a for a in self.rules_countas] + [self.rules_countas[a] for a in self.rules_countas]))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def flatten(lis):
|
||||
|
||||
newlist = []
|
||||
|
||||
for l in lis:
|
||||
if isinstance(l, str):
|
||||
newlist.append(l)
|
||||
else:
|
||||
newlist = newlist + l
|
||||
|
||||
return list(set(newlist))
|
|
@ -1 +0,0 @@
|
|||
!example_file.tsv
|
|
@ -1,6 +0,0 @@
|
|||
# Only the entries in authenticated_machines.tsv are used, this is an example file
|
||||
# It is recommended to have a separate key for every scrobbler application you use,
|
||||
# as well as a key for manual interactions on the website
|
||||
YDzcmp8JpYHCcvJbDOVT7nEDoyCEND6K Chromium Extension on Manjaro
|
||||
correcthorsebatterystaple Pano Scrobbler on Android
|
||||
thingolisthebestking Web Interface
|
Can't render this file because it has a wrong number of fields in line 4.
|
213
compliant_api.py
213
compliant_api.py
|
@ -1,213 +0,0 @@
|
|||
from doreah.logging import log
|
||||
import hashlib
|
||||
import random
|
||||
import database
|
||||
import datetime
|
||||
import itertools
|
||||
import sys
|
||||
from cleanup import CleanerAgent
|
||||
from bottle import response
|
||||
|
||||
## GNU-FM-compliant scrobbling
|
||||
|
||||
|
||||
cla = CleanerAgent()
|
||||
|
||||
def md5(input):
|
||||
m = hashlib.md5()
|
||||
m.update(bytes(input,encoding="utf-8"))
|
||||
return m.hexdigest()
|
||||
|
||||
def generate_key(ls):
|
||||
key = ""
|
||||
for i in range(64):
|
||||
key += str(random.choice(list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
|
||||
ls.append(key)
|
||||
return key
|
||||
|
||||
#def check_sig(keys):
|
||||
# try:
|
||||
# sig = keys.pop("api_sig")
|
||||
# text = "".join([key + keys[key] for key in sorted(keys.keys())]) + # secret
|
||||
# assert sig == md5(text)
|
||||
# return True
|
||||
# except:
|
||||
# return False
|
||||
|
||||
|
||||
handlers = {}
|
||||
|
||||
def handler(apiname,version):
|
||||
def deco(cls):
|
||||
handlers[(apiname,version)] = cls()
|
||||
return cls
|
||||
return deco
|
||||
|
||||
def handle(path,keys):
|
||||
print("API request: " + str(path))
|
||||
print("Keys:")
|
||||
for k in keys:
|
||||
print("\t",k,":",keys.get(k))
|
||||
|
||||
|
||||
if len(path)>1 and (path[0],path[1]) in handlers:
|
||||
handler = handlers[(path[0],path[1])]
|
||||
path = path[2:]
|
||||
try:
|
||||
response.status,result = handler.handle(path,keys)
|
||||
except:
|
||||
type = sys.exc_info()[0]
|
||||
response.status,result = handler.errors[type]
|
||||
else:
|
||||
result = {"error":"Invalid scrobble protocol"}
|
||||
response.status = 500
|
||||
|
||||
|
||||
print("Response: " + str(result))
|
||||
return result
|
||||
|
||||
def scrobbletrack(artiststr,titlestr,timestamp):
|
||||
try:
|
||||
(artists,title) = cla.fullclean(artiststr,titlestr)
|
||||
database.createScrobble(artists,title,timestamp)
|
||||
database.sync()
|
||||
except:
|
||||
raise ScrobblingException()
|
||||
|
||||
|
||||
class BadAuthException(Exception): pass
|
||||
class InvalidAuthException(Exception): pass
|
||||
class InvalidMethodException(Exception): pass
|
||||
class InvalidSessionKey(Exception): pass
|
||||
class MalformedJSONException(Exception): pass
|
||||
class ScrobblingException(Exception): pass
|
||||
|
||||
class APIHandler:
|
||||
# make these classes singletons
|
||||
_instance = None
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not isinstance(cls._instance, cls):
|
||||
cls._instance = object.__new__(cls, *args, **kwargs)
|
||||
return cls._instance
|
||||
|
||||
|
||||
def handle(self,pathnodes,keys):
|
||||
try:
|
||||
methodname = self.get_method(pathnodes,keys)
|
||||
method = self.methods[methodname]
|
||||
except:
|
||||
raise InvalidMethodException()
|
||||
return method(pathnodes,keys)
|
||||
|
||||
@handler("audioscrobbler","2.0")
|
||||
@handler("gnufm","2.0")
|
||||
@handler("gnukebox","2.0")
|
||||
class GNUFM2(APIHandler):
|
||||
def __init__(self):
|
||||
# no need to save these on disk, clients can always request a new session
|
||||
self.mobile_sessions = []
|
||||
self.methods = {
|
||||
"auth.getMobileSession":self.authmobile,
|
||||
"track.scrobble":self.scrobble
|
||||
}
|
||||
self.errors = {
|
||||
BadAuthException:(400,{"error":6,"message":"Requires authentication"}),
|
||||
InvalidAuthException:(401,{"error":4,"message":"Invalid credentials"}),
|
||||
InvalidMethodException:(200,{"error":3,"message":"Invalid method"}),
|
||||
InvalidSessionKey:(403,{"error":9,"message":"Invalid session key"}),
|
||||
ScrobblingException:(500,{"error":8,"message":"Operation failed"})
|
||||
}
|
||||
|
||||
def get_method(self,pathnodes,keys):
|
||||
return keys.get("method")
|
||||
|
||||
def authmobile(self,pathnodes,keys):
|
||||
token = keys.get("authToken")
|
||||
user = keys.get("username")
|
||||
password = keys.get("password")
|
||||
# either username and password
|
||||
if user is not None and password is not None:
|
||||
if password in database.allAPIkeys():
|
||||
sessionkey = generate_key(self.mobile_sessions)
|
||||
return 200,{"session":{"key":sessionkey}}
|
||||
else:
|
||||
raise InvalidAuthException()
|
||||
# or username and token (deprecated by lastfm)
|
||||
elif user is not None and token is not None:
|
||||
for key in database.allAPIkeys():
|
||||
if md5(user + md5(key)) == token:
|
||||
sessionkey = generate_key(self.mobile_sessions)
|
||||
return 200,{"session":{"key":sessionkey}}
|
||||
raise InvalidAuthException()
|
||||
else:
|
||||
raise BadAuthException()
|
||||
|
||||
def scrobble(self,pathnodes,keys):
|
||||
if keys.get("sk") is None or keys.get("sk") not in self.mobile_sessions:
|
||||
raise InvalidSessionKey()
|
||||
else:
|
||||
if "track" in keys and "artist" in keys:
|
||||
artiststr,titlestr = keys["artist"], keys["track"]
|
||||
#(artists,title) = cla.fullclean(artiststr,titlestr)
|
||||
timestamp = int(keys["timestamp"])
|
||||
#database.createScrobble(artists,title,timestamp)
|
||||
scrobbletrack(artiststr,titlestr,timestamp)
|
||||
return 200,{"scrobbles":{"@attr":{"ignored":0}}}
|
||||
else:
|
||||
for num in range(50):
|
||||
if "track[" + str(num) + "]" in keys:
|
||||
artiststr,titlestr = keys["artist[" + str(num) + "]"], keys["track[" + str(num) + "]"]
|
||||
#(artists,title) = cla.fullclean(artiststr,titlestr)
|
||||
timestamp = int(keys["timestamp[" + str(num) + "]"])
|
||||
#database.createScrobble(artists,title,timestamp)
|
||||
scrobbletrack(artiststr,titlestr,timestamp)
|
||||
return 200,{"scrobbles":{"@attr":{"ignored":0}}}
|
||||
|
||||
|
||||
|
||||
@handler("listenbrainz","1")
|
||||
@handler("lbrnz","1")
|
||||
class LBrnz1(APIHandler):
|
||||
def __init__(self):
|
||||
self.methods = {
|
||||
"submit-listens":self.submit
|
||||
}
|
||||
self.errors = {
|
||||
BadAuthException:(401,{"code":401,"error":"You need to provide an Authorization header."}),
|
||||
InvalidAuthException:(401,{"code":401,"error":"Incorrect Authorization"}),
|
||||
InvalidMethodException:(200,{"code":200,"error":"Invalid Method"}),
|
||||
MalformedJSONException:(400,{"code":400,"error":"Invalid JSON document submitted."}),
|
||||
ScrobblingException:(500,{"code":500,"error":"Unspecified server error."})
|
||||
}
|
||||
|
||||
def get_method(self,pathnodes,keys):
|
||||
print(pathnodes)
|
||||
return pathnodes.pop(0)
|
||||
|
||||
def submit(self,pathnodes,keys):
|
||||
try:
|
||||
token = keys.get("Authorization").replace("token ","").replace("Token ","").strip()
|
||||
except:
|
||||
raise BadAuthException()
|
||||
|
||||
if token not in database.allAPIkeys():
|
||||
raise InvalidAuthException()
|
||||
|
||||
try:
|
||||
if keys["listen_type"] in ["single","import"]:
|
||||
payload = keys["payload"]
|
||||
for listen in payload:
|
||||
metadata = listen["track_metadata"]
|
||||
artiststr, titlestr = metadata["artist_name"], metadata["track_name"]
|
||||
#(artists,title) = cla.fullclean(artiststr,titlestr)
|
||||
try:
|
||||
timestamp = int(listen["listened_at"])
|
||||
except:
|
||||
timestamp = int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())
|
||||
#database.createScrobble(artists,title,timestamp)
|
||||
scrobbletrack(artiststr,titlestr,timestamp)
|
||||
return 200,{"code":200,"status":"ok"}
|
||||
else:
|
||||
return 200,{"code":200,"status":"ok"}
|
||||
except:
|
||||
raise MalformedJSONException()
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/with-contenv bash
|
||||
|
||||
if [ "$(s6-setuidgid abc id -u)" = "0" ]; then
|
||||
echo "-------------------------------------"
|
||||
echo "WARN: Running as root! If you meant to do this than this message can be ignored."
|
||||
echo "If you are running this container on a *linux* host and are not using podman rootless you SHOULD"
|
||||
echo "change the ENVs PUID and PGID for this container to ensure correct permissions on your config folder."
|
||||
echo -e "See: https://github.com/krateng/maloja#linux-host\n"
|
||||
echo -e "-------------------------------------\n"
|
||||
fi
|
|
@ -0,0 +1 @@
|
|||
oneshot
|
|
@ -0,0 +1 @@
|
|||
/etc/s6-overlay/s6-rc.d/init-permission-check/run
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# used https://github.com/linuxserver/docker-wikijs/blob/master/root/etc/s6-overlay/s6-rc.d/svc-wikijs/run as a template
|
||||
|
||||
echo -e "\nMaloja is starting!"
|
||||
exec \
|
||||
s6-setuidgid abc python -m maloja run
|
|
@ -0,0 +1 @@
|
|||
longrun
|
1226
database.py
1226
database.py
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
git tag -l '*.0' -n1 --sort=v:refname
|
|
@ -0,0 +1,21 @@
|
|||
import toml
|
||||
import os
|
||||
|
||||
with open("pyproject.toml") as filed:
|
||||
data = toml.load(filed)
|
||||
|
||||
info = {
|
||||
'name':data['project']['name'],
|
||||
'license':"GPLv3",
|
||||
'version':data['project']['version'],
|
||||
'architecture':'all',
|
||||
'description':'"' + data['project']['description'] + '"',
|
||||
'url':'"' + data['project']['urls']['homepage'] + '"',
|
||||
'maintainer':f"\"{data['project']['authors'][0]['name']} <{data['project']['authors'][0]['email']}>\"",
|
||||
}
|
||||
|
||||
|
||||
for target in ["apk","deb"]:
|
||||
lcmd = f"fpm {' '.join(f'--{key} {info[key]}' for key in info)} -s python -t {target} . "
|
||||
print(lcmd)
|
||||
os.system(lcmd)
|
|
@ -0,0 +1,3 @@
|
|||
minor_release_name: "Yura"
|
||||
'1.0':
|
||||
commit: "1fac2ca965fdbe40c85a88559d5b736f4829e7b0"
|
|
@ -0,0 +1,3 @@
|
|||
minor_release_name: "Solar"
|
||||
'1.1':
|
||||
commit: "5603ca9eb137516e604e9e3e83e273a70ef32f65"
|
|
@ -0,0 +1,3 @@
|
|||
minor_release_name: "Jeonghwa"
|
||||
'1.2':
|
||||
commit: "d46d2be2bf27ef40ddd9f0c077f86dcf0214adbb"
|
|
@ -0,0 +1,3 @@
|
|||
minor_release_name: "IU"
|
||||
'1.3':
|
||||
commit: "0bf1790a7cc0174b84f8c25dade6b221b13d65e9"
|
|
@ -0,0 +1,3 @@
|
|||
minor_release_name: "Chungha"
|
||||
'1.4':
|
||||
commit: "981c0e4ae2ad1bff5a0778b6fa34916b0c4d4f4a"
|
|
@ -0,0 +1,3 @@
|
|||
minor_release_name: "Seulgi"
|
||||
'1.5':
|
||||
commit: "e282789153ec3df133474a56e8d922a73795b72a"
|
|
@ -0,0 +1,5 @@
|
|||
minor_release_name: "Irene"
|
||||
'2.0':
|
||||
commit: "55621ef4efdf61c3092d42565e897dfbaa0244c8"
|
||||
notes:
|
||||
- "[Architecture] Refactored into Python Package"
|
|
@ -0,0 +1,5 @@
|
|||
minor_release_name: "Jennie"
|
||||
2.1.0:
|
||||
commit: "b87379ed986640788201f1ff52826413067e5ffb"
|
||||
2.1.4:
|
||||
commit: "c95ce17451cb19b4775a819f82a532d3a3a6231b"
|
|
@ -0,0 +1,17 @@
|
|||
minor_release_name: "Yeri"
|
||||
2.10.0:
|
||||
commit: "ce9d882856be8f6caca14ab7e5b9f13d6c31940b"
|
||||
2.10.1:
|
||||
commit: "f555ee9d9fc485c6a241f4a8fa88bd68527ed2e2"
|
||||
2.10.2:
|
||||
commit: "9a6617b4b1117a6e53d818aadcda886c831e16db"
|
||||
2.10.3:
|
||||
commit: "5b7d1fd8e9f70a8bf9c5bfbe4aca5b796578e114"
|
||||
2.10.4:
|
||||
commit: "3cf0dd9767fe62702b2f5c4f0267a234338a972b"
|
||||
2.10.5:
|
||||
commit: "034bd064f11ef18ebbfcb25bd8acac8eacce1324"
|
||||
2.10.6:
|
||||
commit: "f62fd254dd44deca50a860f3a966390ae9c3662c"
|
||||
2.10.7:
|
||||
commit: "212fbf368e38281f45a0d8dd63dc051dbd8cd8cf"
|
|
@ -0,0 +1,3 @@
|
|||
minor_release_name: "Akali"
|
||||
2.11.0:
|
||||
commit: "218313f80c160f90b28d99236a062ef62db7260d"
|
|
@ -0,0 +1,41 @@
|
|||
minor_release_name: "Tzuyu"
|
||||
2.12.0:
|
||||
commit: "723efcb8ba12f7bda9acc21d81d9930265881c15"
|
||||
2.12.1:
|
||||
commit: "a42ed56d2de47f88f873737f0f1374e99be895bf"
|
||||
2.12.2:
|
||||
commit: "5006ad2bf1a6d5132ed07d28a7f6f0a9a454d5a7"
|
||||
2.12.3:
|
||||
commit: "2a5d9498d1dd7bb6ac62a27d518a87542ec3f344"
|
||||
2.12.4:
|
||||
commit: "06c32e205e95df8d3d1e27876887b7da7aa2bdf4"
|
||||
2.12.5:
|
||||
commit: "c2f8ecc2dfa1ac4febde228fce150e08fb47be38"
|
||||
2.12.6:
|
||||
commit: "a652a22a96ab693a4e7c3271520e6ad79fa025af"
|
||||
2.12.7:
|
||||
commit: "5455abd0d1c9ecc8e000d04257f721babacb18e9"
|
||||
2.12.8:
|
||||
commit: "8ebd27ab76ad2dcaf1dfef0cc171900fa20d5ee5"
|
||||
2.12.9:
|
||||
commit: "49598e914f4e2d7895959bea954238db8a6cee78"
|
||||
2.12.10:
|
||||
commit: "9037403777faa089092cf103181027febf6f0340"
|
||||
2.12.12:
|
||||
commit: "eaaa0f3b53aa102ba1eb709c1803e94752017d86"
|
||||
2.12.13:
|
||||
commit: "c31770a34c96cecc87af78f861819cc49fe98dda"
|
||||
2.12.14:
|
||||
commit: "5157ce825eea9bf7b74123cb02dd28e25c6a0767"
|
||||
2.12.15:
|
||||
commit: "33af60ed2c8c980f17338827a9cb96e7f2fd2572"
|
||||
2.12.16:
|
||||
commit: "26dfdfb569d0beaf4ba8c6c67a9e2295d1362eed"
|
||||
2.12.17:
|
||||
commit: "21012234409c01fec3cb5c506f0b4ba74b735b0b"
|
||||
2.12.18:
|
||||
commit: "59eaa2264aefa6c9ed5f38e8490f77150bcae27b"
|
||||
2.12.19:
|
||||
commit: "8958eb1b547f07d5d063c46cbe59ec57e000ecae"
|
||||
2.12.20:
|
||||
commit: "7774d9a9361db986092e143e1bc397ce7a7524dd"
|
|
@ -0,0 +1,11 @@
|
|||
minor_release_name: "Aqua"
|
||||
2.13.0:
|
||||
commit: "8555b28fbc9a220577260014b7f71f433263cb9f"
|
||||
2.13.1:
|
||||
commit: "cefed03bc95dd5641b918f79b6ed14b2bfc9898d"
|
||||
2.13.2:
|
||||
commit: "0f5ccd4645ead8d1ad48a532752d401424edb236"
|
||||
2.13.3:
|
||||
commit: "40648b66f36894a297633c650e570ac77555d143"
|
||||
2.13.4:
|
||||
commit: "0ccd39ffd99b19e4cd1b1a14f97bfb4385662eeb"
|
|
@ -0,0 +1,23 @@
|
|||
minor_release_name: "Mina"
|
||||
2.14.0:
|
||||
commit: "1b0e3ffdb2389ae6ca484c78840756d0b7e5c0be"
|
||||
2.14.1:
|
||||
commit: "fb2dff8addc7eaf740c5e30cbcd6791aab882c56"
|
||||
2.14.2:
|
||||
commit: "cd8e0ff90abf7d01761b0576c4168254b9b1f7c1"
|
||||
2.14.3:
|
||||
commit: "f806fb8ed24dd1474b80e7b1a9a7637cdbd35905"
|
||||
2.14.4:
|
||||
commit: "868b8396a0a4ff0f687e651772d746af6d9dfab1"
|
||||
2.14.5:
|
||||
commit: "21d1643988e40a02531bcc708f43925789d854d1"
|
||||
2.14.6:
|
||||
commit: "ccbb3d3a807fd77a1481f9d44f311c7f8df659c7"
|
||||
2.14.7:
|
||||
commit: "634df2ffafdfa00b6caf981108d333e30bf160f8"
|
||||
2.14.8:
|
||||
commit: "ec5723d2b3122faaa5b76c5a1b156c9a915af9d6"
|
||||
2.14.9:
|
||||
commit: "2c73c81434e1a591685a4b1d267a9eb6dbd57174"
|
||||
2.14.10:
|
||||
commit: "e152a2edde836f8fb30427d13eb1e9e0d591a00b"
|
|
@ -0,0 +1,11 @@
|
|||
minor_release_name: "Rosé"
|
||||
2.2.0:
|
||||
commit: "33cea26a791e224625aa9bc523e2cf90e39c8a50"
|
||||
2.2.1:
|
||||
commit: "fbce600c4edd2b530e6673b89513b1a26b068b64"
|
||||
2.2.2:
|
||||
commit: "c518627670f5614a2b9931471337a1a6b2ee344f"
|
||||
2.2.3:
|
||||
commit: "a2cc27ddd46c7cf9959f33478eac396e18f90055"
|
||||
2.2.4:
|
||||
commit: "c6deb1543779ce8b09af6bcbdc35e7668af86010"
|
|
@ -0,0 +1,19 @@
|
|||
minor_release_name: "Nancy"
|
||||
2.3.0:
|
||||
commit: "8793b149f501fe5f3e237d7ae0fcd23c8f4e5e9d"
|
||||
2.3.1:
|
||||
commit: "7c6e2ad60f15d8c4ac85a0808a0abd07549a4a2b"
|
||||
2.3.2:
|
||||
commit: "5a08fd78c69c4047b82ff9c394ea23d25356758e"
|
||||
2.3.3:
|
||||
commit: "9cf1fb3ed83817168dfe2ac30a42dcadb080c043"
|
||||
2.3.4:
|
||||
commit: "eb82282e58259b243958e7590506bd26f8e92db0"
|
||||
2.3.5:
|
||||
commit: "a4f13f6923b7783509462944f1abb235b4a068d0"
|
||||
2.3.6:
|
||||
commit: "b611387011e4cbd274e210d0c21c83d15302281c"
|
||||
2.3.7:
|
||||
commit: "b17060184b6897b18cf8af28a3817c9989aac96f"
|
||||
2.3.8:
|
||||
commit: "afe01c8341acd4cf9f4b84fbba85aab6777fd230"
|
|
@ -0,0 +1,29 @@
|
|||
minor_release_name: "Songhee"
|
||||
2.4.0:
|
||||
commit: "6aa65bf1ce273d9fd36d44f6e24439981b2228a3"
|
||||
2.4.1:
|
||||
commit: "b117e6f7ec80afc6210314ce97bac087d5ab7b54"
|
||||
2.4.2:
|
||||
commit: "d989134e65c20ab33b0ea8e4a132655074057757"
|
||||
2.4.3:
|
||||
commit: "9b787fa3b13d77a9cfbe21061f519defac7fafd0"
|
||||
2.4.4:
|
||||
commit: "948772b1c26070d7814871824b970fb60fc6976d"
|
||||
2.4.5:
|
||||
commit: "2da5ab83b3a410b02af48e70b298069218a7e2a3"
|
||||
2.4.6:
|
||||
commit: "65f9c88da4d56df37e4a3f974d7f660502c7a310"
|
||||
2.4.7:
|
||||
commit: "c166620d5f9706e54f9cd67044d42bf8583575d8"
|
||||
2.4.8:
|
||||
commit: "98c1527f778958b1a3322a4f026cfe2c421388aa"
|
||||
2.4.9:
|
||||
commit: "b21b27bb6e230901281bb524f84e177c937b48fd"
|
||||
2.4.10:
|
||||
commit: "08fe4695f6d5ef09789688481db478d0decbd5df"
|
||||
2.4.11:
|
||||
commit: "5c6a901f5118be54ae44affbd6881b14bc30e04a"
|
||||
2.4.12:
|
||||
commit: "6658165baedeee3939084ba4500de3de06bbc045"
|
||||
2.4.13:
|
||||
commit: "57403a89ab1d679523341d6a607d0b03e495ff35"
|
|
@ -0,0 +1,9 @@
|
|||
minor_release_name: "Seungeun"
|
||||
2.5.0:
|
||||
commit: "990131f546876d1461bac745e5cab3e60c78d038"
|
||||
2.5.1:
|
||||
commit: "0918444ab6ff934ba83393e294a135b1fc25bd0c"
|
||||
2.5.2:
|
||||
commit: "0918444ab6ff934ba83393e294a135b1fc25bd0c"
|
||||
2.5.3:
|
||||
commit: "0918444ab6ff934ba83393e294a135b1fc25bd0c"
|
|
@ -0,0 +1,21 @@
|
|||
minor_release_name: "HyunA"
|
||||
2.6.0:
|
||||
commit: "b161da1c1a1632725a44e998ff0d1872b3d5d184"
|
||||
2.6.1:
|
||||
commit: "1eae55e3bba335d41da0d21dfc383b838d9f0d03"
|
||||
2.6.2:
|
||||
commit: "dd3c83920b668466f2c053434bfd6be93bf32942"
|
||||
2.6.3:
|
||||
commit: "27f3ff6d085f42bdb67385f967db904022339d1d"
|
||||
2.6.4:
|
||||
commit: "5f8e73e6c714e9ca94a66f48d1b72fe516bbb0da"
|
||||
2.6.5:
|
||||
commit: "0fdd7669cced6c2b47f657e510bda03a053ee7ae"
|
||||
2.6.6:
|
||||
commit: "87cdb9987efe08b466f99f9ccb8b808131f9fbcd"
|
||||
2.6.7:
|
||||
commit: "0bdc4654bfb0f42d838e15c3d36dab0b4472db00"
|
||||
2.6.8:
|
||||
commit: "bdfb2a4a0b48362aabda7bb735296d83a02b932d"
|
||||
2.6.9:
|
||||
commit: "cb7a6d224152048176e6187ede6d60625961ab39"
|
|
@ -0,0 +1,25 @@
|
|||
minor_release_name: "Shanshan"
|
||||
2.7.0:
|
||||
commit: "8d7fb9a2c8be3f813ee5994be1818f9f81088faa"
|
||||
2.7.1:
|
||||
commit: "6885fbdeccb8b690fa0af59d8fd341e44803798f"
|
||||
2.7.2:
|
||||
commit: "4113d1761e28a7ee3b3cdabe4404cf3876f1fc84"
|
||||
2.7.3:
|
||||
commit: "1563a15abde175022b50fa085c6b9b19a6021c31"
|
||||
2.7.4:
|
||||
commit: "3e6bcc45d55446c6607664e407768391b47c5421"
|
||||
2.7.5:
|
||||
commit: "fa05c406606e269fb4153465611caeb71c12b486"
|
||||
2.7.6:
|
||||
commit: "47087b4288cbfa6000ca019a000f27ee5846d161"
|
||||
2.7.7:
|
||||
commit: "379ee49f1c61df9720346d3d021dea040587d54d"
|
||||
2.7.8:
|
||||
commit: "75bd823ad0cc24efecd1de193436a28dfaecd4f3"
|
||||
2.7.9:
|
||||
commit: "fb04dd507cee42092b889fe72cdf9975ea48e3b1"
|
||||
2.7.10:
|
||||
commit: "7fc879f77818371721e21c13e9df98796cf632de"
|
||||
2.7.11:
|
||||
commit: "44a2739a3b6e58cb90b7f7dfca2197834cf30464"
|
|
@ -0,0 +1,13 @@
|
|||
minor_release_name: "Haeun"
|
||||
2.8.0:
|
||||
commit: "25661f82af9338a024aae429cdafec7c86692aa5"
|
||||
2.8.1:
|
||||
commit: "1321fcb45ebe0291c9fd47ff2eb8cc329035acf3"
|
||||
2.8.2:
|
||||
commit: "e27a83bdc99a06a207c67c6f0034bc0a554c89af"
|
||||
2.8.3:
|
||||
commit: "6acab324dbd3594dcfbf944bfdfb5c8fe173354b"
|
||||
2.8.4:
|
||||
commit: "f7f1b1225e64b54d8962467182bddcc1de237f51"
|
||||
2.8.5:
|
||||
commit: "1dbc0b7fca05830d654076c74a91b6b74f470d5b"
|
|
@ -0,0 +1,23 @@
|
|||
minor_release_name: "Yaorenmao"
|
||||
2.9.0:
|
||||
commit: "8b4e9609e994d74506fd91471bd5a622b75b2f08"
|
||||
2.9.1:
|
||||
commit: "52a9faae90175841b2c259dd4677697e513e12f9"
|
||||
2.9.2:
|
||||
commit: "5cf7ca2e9bbf66082c4afb76b4033ff17c9cf8c8"
|
||||
2.9.3:
|
||||
commit: "e8c316f1992c3e5f171891272f32d959bb1fa4f0"
|
||||
2.9.4:
|
||||
commit: "e8a87cb8a5e2f63850ff3c02ed5aa8ee388460ed"
|
||||
2.9.5:
|
||||
commit: "09d3f103832bb7e26949a8f2df60c25851886bdc"
|
||||
2.9.6:
|
||||
commit: "9fb352cc6fe2bc41c56304e5ba941035fc1ac82d"
|
||||
2.9.7:
|
||||
commit: "f4a563f080f7dba336034feb1c0c42057f8d8d8c"
|
||||
2.9.8:
|
||||
commit: "2da9f154be240b8648d68a7eb2a3291738cfc93c"
|
||||
2.9.9:
|
||||
commit: "f7861c44b4a44b0cdd34e9f3f62530b8bf2837e3"
|
||||
2.9.10:
|
||||
commit: "22172d8b57df2ad1282f8d835183be45843fdd6a"
|
|
@ -0,0 +1,51 @@
|
|||
minor_release_name: "Yeonhee"
|
||||
3.0.0:
|
||||
commit: "f31c95228eb2dc01e661be928ffd881c063377da"
|
||||
notes:
|
||||
- "[Architecture] Switched to SQLite for main database"
|
||||
- "[Architecture] Switched to SQLite for artwork cache"
|
||||
- "[Feature] Added scrobble deletion from web interface"
|
||||
3.0.1:
|
||||
commit: "700b81217cb585df631d6f069243c56074cd1b71"
|
||||
notes:
|
||||
- "[Bugfix] Fixed upgrading imported scrobbles"
|
||||
3.0.2:
|
||||
commit: "4a8221f7a08f679b21c1fb619f03e5f922a1dc2b"
|
||||
notes:
|
||||
- "[Logging] Cleaned up output for waitress warnings"
|
||||
- "[Bugfix] Fixed exception in native API"
|
||||
3.0.3:
|
||||
commit: "1d9247fc724d7410b6e50d2cbfaa8f375d5e70af"
|
||||
notes:
|
||||
- "[Documentation] Added descriptions for native API endpoints"
|
||||
- "[Code Health] Made arguments for native API scrobbling explicit"
|
||||
- "[Bugfix] Fixed faulty entity type recognition for artists including the string 'artists'"
|
||||
- "[Bugfix] Fixed OS return codes"
|
||||
3.0.4:
|
||||
commit: "206ebd58ea204e0008f2c9bf72d76dd9918fec53"
|
||||
notes:
|
||||
- "[Feature] Enabled dual stack for web server"
|
||||
- "[Feature] Added better feedback to native API endpoints"
|
||||
- "[Bugfix] Fixed native API receiving superfluous keywords"
|
||||
- "[Bugfix] Fixed crash when importing scrobbles with artists with similar names"
|
||||
3.0.5:
|
||||
commit: "fe21894c5ecf3a53c9c5c00453abfc7f41c6a83e"
|
||||
notes:
|
||||
- "[Feature] Added notification system for web interface"
|
||||
- "[Bugfix] Fixed crash when encountering error in Lastfm import"
|
||||
3.0.6:
|
||||
commit: "b3d4cb7a153845d1f5a5eef67a6508754e338f2f"
|
||||
notes:
|
||||
- "[Performance] Implemented search in database"
|
||||
- "[Bugfix] Better parsing of featuring artists"
|
||||
- "[Bugfix] Fixed buffered output in Docker"
|
||||
- "[Bugfix] Fixed importing a Spotify file without path"
|
||||
- "[Bugfix] No longer releasing database lock during scrobble creation"
|
||||
- "[Distribution] Experimental arm64 image"
|
||||
3.0.7:
|
||||
commit: "62abc319303a6cb6463f7c27b6ef09b76fc67f86"
|
||||
notes:
|
||||
- "[Bugix] Improved signal handling"
|
||||
- "[Bugix] Fixed constant re-caching of all-time stats, significantly increasing page load speed"
|
||||
- "[Logging] Disabled cache information when cache is not used"
|
||||
- "[Distribution] Experimental arm/v7 image"
|
|
@ -0,0 +1,46 @@
|
|||
minor_release_name: "Soyeon"
|
||||
3.1.0:
|
||||
commit: "bfa553bed05d7dba33f611a44485d6cf460ba308"
|
||||
notes:
|
||||
- "[Architecture] Cleaned up legacy process control"
|
||||
- "[Architecture] Added proper exception framework to native API"
|
||||
- "[Feature] Implemented track title and artist name editing from web interface"
|
||||
- "[Feature] Implemented track and artist merging from web interface"
|
||||
- "[Feature] Implemented scrobble reparsing from web interface"
|
||||
- "[Performance] Adjusted cache sizes"
|
||||
- "[Logging] Added cache memory use information"
|
||||
- "[Technical] Bumped Python Version and various dependencies"
|
||||
3.1.1:
|
||||
commit: "20aae955b2263be07c56bafe4794f622117116ef"
|
||||
notes:
|
||||
- "[Bugfix] Fixed inclusion of custom css files"
|
||||
- "[Bugfix] Fixed list values in configuration"
|
||||
3.1.2:
|
||||
commit: "a0739306013cd9661f028fb5b2620cfa2d298aa4"
|
||||
notes:
|
||||
- "[Feature] Added remix artist parsing"
|
||||
- "[Feature] Added API debug mode"
|
||||
- "[Bugfix] Fixed leftover whitespaces when parsing titles"
|
||||
- "[Bugfix] Fixed handling of fallthrough values in config file"
|
||||
3.1.3:
|
||||
commit: "f3a04c79b1c37597cdf3cafcd95e3c923cd6a53f"
|
||||
notes:
|
||||
- "[Bugfix] Fixed infinite recursion with capitalized featuring delimiters"
|
||||
- "[Bugfix] Fixed favicon display"
|
||||
3.1.4:
|
||||
commit: "ef06f2262205c903e7c3060e2d2d52397f8ffc9d"
|
||||
notes:
|
||||
- "[Feature] Expanded information saved from Listenbrainz API"
|
||||
- "[Feature] Added import for Listenbrainz exports"
|
||||
- "[Bugfix] Sanitized artists and tracks with html-like structure"
|
||||
3.1.5:
|
||||
commit: "4330b0294bc0a01cdb841e2e3db370108da901db"
|
||||
notes:
|
||||
- "[Feature] Made image upload part of regular API"
|
||||
- "[Bugfix] Additional entity name sanitization"
|
||||
- "[Bugfix] Fixed image display on Safari"
|
||||
- "[Bugfix] Fixed entity editing on Firefox"
|
||||
- "[Bugfix] Made compatibile with SQLAlchemy 2.0"
|
||||
upcoming:
|
||||
notes:
|
||||
- "[Bugfix] Fixed configuration of time format"
|
|
@ -0,0 +1,2 @@
|
|||
docker build -t maloja . -f Containerfile
|
||||
docker run --rm -p 42010:42010 -v $PWD/testdata:/mlj -e MALOJA_DATA_DIRECTORY=/mlj maloja
|
|
@ -0,0 +1,2 @@
|
|||
podman build -t maloja . -f Containerfile
|
||||
podman run --rm -p 42010:42010 -v $PWD/testdata:/mlj -e MALOJA_DATA_DIRECTORY=/mlj maloja
|
|
@ -0,0 +1,36 @@
|
|||
# Contributor: Johannes Krattenmacher <maloja@dev.krateng.ch>
|
||||
# Maintainer: Johannes Krattenmacher <maloja@dev.krateng.ch>
|
||||
pkgname={{ tool.flit.module.name }}
|
||||
pkgver={{ project.version }}
|
||||
pkgrel=0
|
||||
pkgdesc="{{ project.description }}"
|
||||
url="{{ project.urls.homepage }}"
|
||||
arch="noarch"
|
||||
license="GPL-3.0"
|
||||
depends="{{ tool.osreqs.alpine.run | join(' ') }}"
|
||||
pkgusers=$pkgname
|
||||
pkggroups=$pkgname
|
||||
depends_dev="{{ tool.osreqs.alpine.build | join(' ') }}"
|
||||
makedepends="$depends_dev"
|
||||
source="
|
||||
$pkgname-$pkgver.tar.gz::{{ project.urls.repository }}/archive/refs/tags/v$pkgver.tar.gz
|
||||
"
|
||||
builddir="$srcdir"/$pkgname-$pkgver
|
||||
|
||||
|
||||
|
||||
build() {
|
||||
cd $builddir
|
||||
python3 -m build .
|
||||
pip3 install dist/*.tar.gz
|
||||
}
|
||||
|
||||
package() {
|
||||
mkdir -p /etc/$pkgname || return 1
|
||||
mkdir -p /var/lib/$pkgname || return 1
|
||||
mkdir -p /var/cache/$pkgname || return 1
|
||||
mkdir -p /var/logs/$pkgname || return 1
|
||||
}
|
||||
|
||||
# TODO
|
||||
sha512sums="a674eaaaa248fc2b315514d79f9a7a0bac6aa1582fe29554d9176e8b551e8aa3aa75abeebdd7713e9e98cc987e7bd57dc7a5e9a2fb85af98b9c18cb54de47bf7 $pkgname-${pkgver}.tar.gz"
|
|
@ -0,0 +1,40 @@
|
|||
FROM alpine:3.15
|
||||
# Python image includes two Python versions, so use base Alpine
|
||||
|
||||
# Based on the work of Jonathan Boeckel <jonathanboeckel1996@gmail.com>
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install run dependencies first
|
||||
RUN apk add --no-cache {{ tool.osreqs.alpine.run | join(' ') }}
|
||||
|
||||
# system pip could be removed after build, but apk then decides to also remove all its
|
||||
# python dependencies, even if they are explicitly installed as python packages
|
||||
# whut
|
||||
RUN \
|
||||
apk add py3-pip && \
|
||||
pip install wheel
|
||||
|
||||
|
||||
COPY ./requirements.txt ./requirements.txt
|
||||
|
||||
RUN \
|
||||
apk add --no-cache --virtual .build-deps {{ tool.osreqs.alpine.build | join(' ') }} && \
|
||||
pip install --no-cache-dir -r requirements.txt && \
|
||||
apk del .build-deps
|
||||
|
||||
|
||||
# no chance for caching below here
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip install /usr/src/app
|
||||
|
||||
# Docker-specific configuration
|
||||
# defaulting to IPv4 is no longer necessary (default host is dual stack)
|
||||
ENV MALOJA_SKIP_SETUP=yes
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
EXPOSE 42010
|
||||
# use exec form for better signal handling https://docs.docker.com/engine/reference/builder/#entrypoint
|
||||
ENTRYPOINT ["maloja", "run"]
|
|
@ -0,0 +1,4 @@
|
|||
{% include 'install/install_dependencies_alpine.sh.jinja' %}
|
||||
apk add py3-pip
|
||||
pip install wheel
|
||||
pip install malojaserver
|
|
@ -0,0 +1,4 @@
|
|||
{% include 'install/install_dependencies_debian.sh.jinja' %}
|
||||
apt install python3-pip
|
||||
pip install wheel
|
||||
pip install malojaserver
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
apk update
|
||||
apk add \
|
||||
{{ (tool.osreqs.alpine.build + tool.osreqs.alpine.run + tool.osreqs.alpine.opt) | join(' \\\n\t') }}
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
apt update
|
||||
apt install \
|
||||
{{ (tool.osreqs.debian.build + tool.osreqs.debian.run + tool.osreqs.debian.opt) | join(' \\\n\t') }}
|
|
@ -0,0 +1,3 @@
|
|||
{% for dep in project.dependencies -%}
|
||||
{{ dep }}
|
||||
{% endfor %}
|
|
@ -0,0 +1,3 @@
|
|||
{% for dep in project['optional-dependencies'].full -%}
|
||||
{{ dep }}
|
||||
{% endfor %}
|
|
@ -0,0 +1,907 @@
|
|||
{
|
||||
"info": {
|
||||
"_postman_id": "71b19d5e-78a1-4a4a-98b5-12cbf2f13fe6",
|
||||
"name": "Maloja",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Scrobbling",
|
||||
"item": [
|
||||
{
|
||||
"name": "Scrobble Native",
|
||||
"item": [
|
||||
{
|
||||
"name": "Query String",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": []
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/mlj_1/newscrobble?key={{api_key}}&artist={{data.artist1}}&title={{data.title1}}&artist={{data.artist2}}",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"apis",
|
||||
"mlj_1",
|
||||
"newscrobble"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "{{api_key}}"
|
||||
},
|
||||
{
|
||||
"key": "artist",
|
||||
"value": "{{data.artist1}}"
|
||||
},
|
||||
{
|
||||
"key": "title",
|
||||
"value": "{{data.title1}}"
|
||||
},
|
||||
{
|
||||
"key": "artist",
|
||||
"value": "{{data.artist2}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Query String Redirect",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{url}}/api/newscrobble?key={{api_key}}&artist={{data.artist1}}&title={{data.title1}}&artist={{data.artist2}}",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"newscrobble"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "{{api_key}}"
|
||||
},
|
||||
{
|
||||
"key": "artist",
|
||||
"value": "{{data.artist1}}"
|
||||
},
|
||||
{
|
||||
"key": "title",
|
||||
"value": "{{data.title1}}"
|
||||
},
|
||||
{
|
||||
"key": "artist",
|
||||
"value": "{{data.artist2}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Formdata",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "{{api_key}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "artist",
|
||||
"value": "{{data.artist1}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "title",
|
||||
"value": "{{data.title1}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "artist",
|
||||
"value": "{{data.artist2}}",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/mlj_1/newscrobble",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"apis",
|
||||
"mlj_1",
|
||||
"newscrobble"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Formdata Redirect",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": [
|
||||
{
|
||||
"key": "key",
|
||||
"value": "{{api_key}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "artist",
|
||||
"value": "{{data.artist1}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "title",
|
||||
"value": "{{data.title1}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "artist",
|
||||
"value": "{{data.artist2}}",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/api/newscrobble",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"newscrobble"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "JSON",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.title1}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/mlj_1/newscrobble",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"apis",
|
||||
"mlj_1",
|
||||
"newscrobble"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "JSON Multiartist",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"type": "text",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"key\": \"{{api_key}}\",\n \"artists\": [\"{{data.artist1}}\",\"{{data.artist2}}\"],\n \"title\": \"{{data.title1}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/mlj_1/newscrobble",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"apis",
|
||||
"mlj_1",
|
||||
"newscrobble"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "JSON Redirect",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"type": "text",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"key\": \"{{api_key}}\",\n \"artist\": \"{{data.artist1}}\",\n \"title\": \"{{data.title1}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/api/newscrobble",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"newscrobble"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Scrobble Listenbrainz",
|
||||
"item": [
|
||||
{
|
||||
"name": "JSON",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "token {{api_key}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"listen_type\":\"single\",\n\t\"payload\":[\n\t\t{\n\t\t\t\"track_metadata\":{\n\t\t\t\t\"artist_name\":\"{{data.artist1}}\",\n\t\t\t\t\"track_name\":\"{{data.title1}}\"\n\t\t\t}\n\t\t}\n\t]\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/listenbrainz/1/submit-listens",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"apis",
|
||||
"listenbrainz",
|
||||
"1",
|
||||
"submit-listens"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "JSON Redirect",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"type": "text",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"type": "text",
|
||||
"value": "token {{api_key}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"listen_type\":\"single\",\n\t\"payload\":[\n\t\t{\n\t\t\t\"track_metadata\":{\n\t\t\t\t\"artist_name\":\"{{data.artist1}}\",\n\t\t\t\t\"track_name\":\"{{data.title1}}\"\n\t\t\t}\n\t\t}\n\t]\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/api/s/listenbrainz/1/submit-listens",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"s",
|
||||
"listenbrainz",
|
||||
"1",
|
||||
"submit-listens"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Scrobble Audioscrobbler",
|
||||
"item": [
|
||||
{
|
||||
"name": "JSON",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
""
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"exec": [
|
||||
""
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"method\":\"track.scrobble\",\n\t\"artist\":\"{{data.artist1}}\",\n\t\"track\":\"{{data.title1}}\",\n\t\"sk\":\"{{session_key}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/audioscrobbler/2.0/",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"apis",
|
||||
"audioscrobbler",
|
||||
"2.0",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "JSON Redirect",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
""
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"type": "text",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"method\":\"track.scrobble\",\n\t\"artist\":\"{{data.artist1}}\",\n\t\"track\":\"{{data.title1}}\",\n\t\"sk\":\"{{session_key}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/api/s/audioscrobbler/2.0/",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"s",
|
||||
"audioscrobbler",
|
||||
"2.0",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Authorize",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"var data = JSON.parse(responseBody);",
|
||||
"pm.environment.set(\"session_key\", data.session.key);",
|
||||
"tests[\"gotkey\"] = true;"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n\t\"method\":\"auth.getMobileSession\",\n\t\"authToken\":\"abc\",\n\t\"username\":\"someguy\",\n\t\"password\":\"{{api_key}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/audioscrobbler/2.0/",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"apis",
|
||||
"audioscrobbler",
|
||||
"2.0",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Scrobble Audioscrobbler Legacy",
|
||||
"item": [
|
||||
{
|
||||
"name": "Authorize",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"var data = responseBody.split(\"\\n\");",
|
||||
"",
|
||||
"pm.environment.set(\"session_key\", data[1]);",
|
||||
"pm.environment.set(\"scrobble_url\", data[3]);"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"exec": [
|
||||
"apikey = pm.variables.get(\"api_key\");",
|
||||
"ts = pm.variables.get(\"data.timestamp1\");",
|
||||
"",
|
||||
"token = CryptoJS.MD5(CryptoJS.MD5(apikey) + ts).toString()",
|
||||
"pm.environment.set(\"legacy_token\", token);"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"protocolProfileBehavior": {
|
||||
"disableBodyPruning": true
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": ""
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{url}}/apis/audioscrobbler_legacy/?hs=true&t={{data.timestamp1}}&a={{legacy_token}}",
|
||||
"host": [
|
||||
"{{url}}"
|
||||
],
|
||||
"path": [
|
||||
"apis",
|
||||
"audioscrobbler_legacy",
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "hs",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"key": "t",
|
||||
"value": "{{data.timestamp1}}"
|
||||
},
|
||||
{
|
||||
"key": "a",
|
||||
"value": "{{legacy_token}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Scrobble",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{scrobble_url}}?s={{session_key}}&a[0]={{data.artist1}}&t[0]={{data.title1}}&a[1]={{data.artist2}}&t[1]={{data.title2}}&i[0]={{data.timestamp1}}&i[1]={{data.timestamp2}}",
|
||||
"host": [
|
||||
"{{scrobble_url}}"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "s",
|
||||
"value": "{{session_key}}"
|
||||
},
|
||||
{
|
||||
"key": "a[0]",
|
||||
"value": "{{data.artist1}}"
|
||||
},
|
||||
{
|
||||
"key": "t[0]",
|
||||
"value": "{{data.title1}}"
|
||||
},
|
||||
{
|
||||
"key": "a[1]",
|
||||
"value": "{{data.artist2}}"
|
||||
},
|
||||
{
|
||||
"key": "t[1]",
|
||||
"value": "{{data.title2}}"
|
||||
},
|
||||
{
|
||||
"key": "i[0]",
|
||||
"value": "{{data.timestamp1}}"
|
||||
},
|
||||
{
|
||||
"key": "i[1]",
|
||||
"value": "{{data.timestamp2}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Metadata",
|
||||
"item": [
|
||||
{
|
||||
"name": "Spotify",
|
||||
"item": [
|
||||
{
|
||||
"name": "Authorize",
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"exec": [
|
||||
"apiid = pm.collectionVariables.get(\"external.spotify.api_id\");",
|
||||
"apisecret = pm.collectionVariables.get(\"external.spotify.api_secret\");",
|
||||
"",
|
||||
"authb64 = new Buffer(apiid + ':' + apisecret).toString('base64');",
|
||||
"pm.environment.set(\"authb64\", authb64);",
|
||||
""
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"var data = JSON.parse(responseBody);",
|
||||
"pm.collectionVariables.set(\"external.spotify.access_token\", data.access_token);"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Basic {{authb64}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "urlencoded",
|
||||
"urlencoded": [
|
||||
{
|
||||
"key": "grant_type",
|
||||
"value": "client_credentials",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "https://accounts.spotify.com/api/token",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"accounts",
|
||||
"spotify",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Track Info Old",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "https://api.spotify.com/v1/search?type=track&access_token={{external.spotify.access_token}}&q=artist:{{data.artist3}} track:{{data.title3}}",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"spotify",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"search"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
"value": "track"
|
||||
},
|
||||
{
|
||||
"key": "access_token",
|
||||
"value": "{{external.spotify.access_token}}"
|
||||
},
|
||||
{
|
||||
"key": "q",
|
||||
"value": "artist:{{data.artist3}} track:{{data.title3}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Track Info New",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{external.spotify.access_token}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "https://api.spotify.com/v1/search?type=track&q=artist:{{data.artist3}}%20track:{{data.title3}}",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"api",
|
||||
"spotify",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
"v1",
|
||||
"search"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
"value": "track"
|
||||
},
|
||||
{
|
||||
"key": "q",
|
||||
"value": "artist:{{data.artist3}}%20track:{{data.title3}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Scrobbleforward",
|
||||
"item": [
|
||||
{
|
||||
"name": "Last.fm",
|
||||
"item": [
|
||||
{
|
||||
"name": "Authorize",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "urlencoded",
|
||||
"urlencoded": []
|
||||
},
|
||||
"url": {
|
||||
"raw": "https://ws.audioscrobbler.com/2.0/?method=auth.getMobileSession&username={{external.lastfm.username}}&password={{external.lastfm.password}}&api_key={{external.lastfm.api_key}}&api_sig=TODO",
|
||||
"protocol": "https",
|
||||
"host": [
|
||||
"ws",
|
||||
"audioscrobbler",
|
||||
"com"
|
||||
],
|
||||
"path": [
|
||||
"2.0",
|
||||
""
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "method",
|
||||
"value": "auth.getMobileSession"
|
||||
},
|
||||
{
|
||||
"key": "username",
|
||||
"value": "{{external.lastfm.username}}"
|
||||
},
|
||||
{
|
||||
"key": "password",
|
||||
"value": "{{external.lastfm.password}}"
|
||||
},
|
||||
{
|
||||
"key": "api_key",
|
||||
"value": "{{external.lastfm.api_key}}"
|
||||
},
|
||||
{
|
||||
"key": "api_sig",
|
||||
"value": "TODO"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"var current_timestamp = Math.floor(Date.now() / 1000);",
|
||||
"pm.collectionVariables.set(\"data.timestamp1\", current_timestamp);",
|
||||
"pm.collectionVariables.set(\"data.timestamp2\", current_timestamp - 200);"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "url",
|
||||
"value": "http://localhost:42010"
|
||||
},
|
||||
{
|
||||
"key": "api_key",
|
||||
"value": "localdevtestkey"
|
||||
},
|
||||
{
|
||||
"key": "session_key",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "data.artist1",
|
||||
"value": "EXID ft. Jeremy Soule"
|
||||
},
|
||||
{
|
||||
"key": "data.artist2",
|
||||
"value": "BLACKPINK ft. Tzuyu"
|
||||
},
|
||||
{
|
||||
"key": "data.artist3",
|
||||
"value": "TWICE"
|
||||
},
|
||||
{
|
||||
"key": "data.title1",
|
||||
"value": "Why is the Rum gone?"
|
||||
},
|
||||
{
|
||||
"key": "data.title2",
|
||||
"value": "POP/STARS"
|
||||
},
|
||||
{
|
||||
"key": "data.title3",
|
||||
"value": "One in a Million"
|
||||
},
|
||||
{
|
||||
"key": "data.timestamp1",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "data.timestamp2",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "external.spotify.api_id",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "external.spotify.api_secret",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "external.spotify.access_token",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "external.lastfm.username",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "external.lastfm.password",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "external.lastfm.api_key",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "external.lastfm.secret",
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import threading
|
||||
import subprocess
|
||||
import time
|
||||
import requests
|
||||
import os
|
||||
|
||||
ACTIVE = True
|
||||
|
||||
build_cmd = ["docker","build","-t","maloja",".","-f","Containerfile"]
|
||||
subprocess.run(build_cmd)
|
||||
|
||||
common_prc = (
|
||||
["docker","run","--rm","-v",f"{os.path.abspath('./testdata')}:/mlj","-e","MALOJA_DATA_DIRECTORY=/mlj"],
|
||||
["maloja"]
|
||||
)
|
||||
|
||||
servers = [
|
||||
{'port': 42010},
|
||||
{'port': 42011, 'extraargs':["--memory=1g"]},
|
||||
{'port': 42012, 'extraargs':["--memory=500m"]}
|
||||
]
|
||||
for s in servers:
|
||||
cmd = common_prc[0] + ["-p",f"{s['port']}:42010"] + s.get('extraargs',[]) + common_prc[1]
|
||||
print(cmd)
|
||||
t = threading.Thread(target=subprocess.run,args=(cmd,))
|
||||
s['thread'] = t
|
||||
t.daemon = True
|
||||
t.start()
|
||||
time.sleep(5)
|
||||
|
||||
time.sleep(5)
|
||||
while ACTIVE:
|
||||
time.sleep(1)
|
||||
try:
|
||||
for s in servers:
|
||||
requests.get(f"http://localhost:{s['port']}")
|
||||
except KeyboardInterrupt:
|
||||
ACTIVE = False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for s in servers:
|
||||
s['thread'].join()
|
|
@ -0,0 +1,33 @@
|
|||
import toml
|
||||
import os
|
||||
import jinja2
|
||||
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader('dev/templates'),
|
||||
autoescape=jinja2.select_autoescape(['html', 'xml']),
|
||||
keep_trailing_newline=True
|
||||
)
|
||||
|
||||
with open("pyproject.toml") as filed:
|
||||
data = toml.load(filed)
|
||||
|
||||
templatedir = "./dev/templates"
|
||||
|
||||
for root,dirs,files in os.walk(templatedir):
|
||||
|
||||
reldirpath = os.path.relpath(root,start=templatedir)
|
||||
for f in files:
|
||||
|
||||
relfilepath = os.path.join(reldirpath,f)
|
||||
|
||||
if not f.endswith('.jinja'): continue
|
||||
|
||||
srcfile = os.path.join(root,f)
|
||||
trgfile = os.path.join(reldirpath,f.replace(".jinja",""))
|
||||
|
||||
|
||||
template = env.get_template(relfilepath)
|
||||
result = template.render(**data)
|
||||
|
||||
with open(trgfile,"w") as filed:
|
||||
filed.write(result)
|
|
@ -0,0 +1,9 @@
|
|||
ICON_DIR=./maloja/web/static/png;
|
||||
SCROBBLER_DIR=./auxiliary/chromium_scrobbler;
|
||||
|
||||
convert $ICON_DIR/favicon_large.png -resize 256 $SCROBBLER_DIR/maloja-scrobbler/icon256.png
|
||||
convert $ICON_DIR/favicon_large.png -resize 128 $SCROBBLER_DIR/maloja-scrobbler/icon128.png
|
||||
convert $ICON_DIR/favicon_large.png -resize 48 $SCROBBLER_DIR/maloja-scrobbler/icon48.png
|
||||
convert $ICON_DIR/favicon_large.png -background none -resize 280 -gravity center -extent 440x280 -background "#232327" -flatten $SCROBBLER_DIR/tile.png
|
||||
rm $SCROBBLER_DIR/maloja-scrobbler.zip
|
||||
zip $SCROBBLER_DIR/maloja-scrobbler.zip $SCROBBLER_DIR/maloja-scrobbler/* $SCROBBLER_DIR/maloja-scrobbler/*/*
|
|
@ -0,0 +1,53 @@
|
|||
import os
|
||||
import subprocess as sp
|
||||
import yaml
|
||||
|
||||
FOLDER = "dev/releases"
|
||||
|
||||
releases = {}
|
||||
for f in os.listdir(FOLDER):
|
||||
if f == "branch.yml": continue
|
||||
#maj,min = (int(i) for i in f.split('.')[:2])
|
||||
|
||||
with open(os.path.join(FOLDER,f)) as fd:
|
||||
data = yaml.safe_load(fd)
|
||||
|
||||
name = data.pop('minor_release_name')
|
||||
|
||||
for tag in data:
|
||||
tagtup = tuple(int(i) for i in tag.split('.'))
|
||||
releases[tagtup] = data[tag]
|
||||
|
||||
# this is a bit dirty, works on our data
|
||||
if len(tagtup)<3 or tagtup[2] == 0: releases[tagtup]['name'] = name
|
||||
|
||||
|
||||
for version in releases:
|
||||
|
||||
info = releases[version]
|
||||
version = '.'.join(str(v) for v in version)
|
||||
msg = [
|
||||
f"Version {version}" + (f" '{info.get('name')}'" if info.get('name') else ''),
|
||||
*([""] if info.get('notes') else []),
|
||||
*[f"* {n}" for n in info.get('notes',[])]
|
||||
]
|
||||
|
||||
|
||||
cmd = [
|
||||
'git','tag','--force',
|
||||
'-a',f'v{version}',
|
||||
'-m',
|
||||
'\n'.join(msg),
|
||||
info['commit']
|
||||
]
|
||||
|
||||
try:
|
||||
prev_tag = sp.check_output(["git","show",f'v{maj}.{min}.{hot}']).decode()
|
||||
prev_tag_commit = prev_tag.split('\n')[6].split(" ")[1]
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
assert prev_tag_commit == info['commit']
|
||||
|
||||
print(cmd)
|
||||
sp.run(cmd)
|
|
@ -0,0 +1,20 @@
|
|||
services:
|
||||
maloja:
|
||||
# from dockerhub
|
||||
image: "krateng/maloja:latest"
|
||||
# or built locally
|
||||
#build:
|
||||
# context: .
|
||||
# dockerfile: ./Containerfile
|
||||
ports:
|
||||
- "42010:42010"
|
||||
# different directories for configuration, state and logs
|
||||
volumes:
|
||||
- "$PWD/config:/etc/maloja"
|
||||
- "$PWD/data:/var/lib/maloja"
|
||||
- "$PWD/logs:/var/log/maloja"
|
||||
#you can also have everything together instead:
|
||||
#volumes:
|
||||
#- "$PWD/data:/data"
|
||||
#environment:
|
||||
#- "MALOJA_DATA_DIRECTORY=/data"
|
132
external.py
132
external.py
|
@ -1,132 +0,0 @@
|
|||
import urllib.parse, urllib.request
|
||||
import json
|
||||
import base64
|
||||
from doreah.settings import get_settings
|
||||
from doreah.logging import log
|
||||
|
||||
|
||||
apis_artists = []
|
||||
|
||||
if get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"]:
|
||||
apis_artists.append({
|
||||
"name":"LastFM + Fanart.tv",
|
||||
#"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"] and get_settings("FANARTTV_API_KEY") not in [None,"ASK"],
|
||||
"steps":[
|
||||
("get","http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&artist={artiststring}&api_key=" + str(get_settings("LASTFM_API_KEY")) + "&format=json"),
|
||||
("parse",["artist","mbid"]),
|
||||
("get","http://webservice.fanart.tv/v3/music/{var}?api_key=" + str(get_settings("FANARTTV_API_KEY"))),
|
||||
("parse",["artistthumb",0,"url"])
|
||||
]
|
||||
})
|
||||
|
||||
if get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"]:
|
||||
apis_artists.append({
|
||||
"name":"Spotify",
|
||||
#"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
|
||||
"steps":[
|
||||
("post","https://accounts.spotify.com/api/token",{"Authorization":"Basic " + base64.b64encode(bytes(get_settings("SPOTIFY_API_ID") + ":" + get_settings("SPOTIFY_API_SECRET"),encoding="utf-8")).decode("utf-8")},{"grant_type":"client_credentials"}),
|
||||
("parse",["access_token"]),
|
||||
("get","https://api.spotify.com/v1/search?q={artiststring}&type=artist&access_token={var}"),
|
||||
("parse",["artists","items",0,"images",0,"url"])
|
||||
]
|
||||
})
|
||||
|
||||
apis_tracks = []
|
||||
|
||||
if get_settings("LASTFM_API_KEY") not in [None,"ASK"]:
|
||||
apis_tracks.append({
|
||||
"name":"LastFM",
|
||||
#"check":get_settings("LASTFM_API_KEY") not in [None,"ASK"],
|
||||
"steps":[
|
||||
("get","https://ws.audioscrobbler.com/2.0/?method=track.getinfo&track={titlestring}&artist={artiststring}&api_key=" + get_settings("LASTFM_API_KEY") + "&format=json"),
|
||||
("parse",["track","album","image",3,"#text"])
|
||||
]
|
||||
})
|
||||
|
||||
if get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"]:
|
||||
apis_tracks.append({
|
||||
"name":"Spotify",
|
||||
#"check":get_settings("SPOTIFY_API_ID") not in [None,"ASK"] and get_settings("SPOTIFY_API_SECRET") not in [None,"ASK"],
|
||||
"steps":[
|
||||
("post","https://accounts.spotify.com/api/token",{"Authorization":"Basic " + base64.b64encode(bytes(get_settings("SPOTIFY_API_ID") + ":" + get_settings("SPOTIFY_API_SECRET"),encoding="utf-8")).decode("utf-8")},{"grant_type":"client_credentials"}),
|
||||
("parse",["access_token"]),
|
||||
("get","https://api.spotify.com/v1/search?q={artiststring}%20{titlestring}&type=track&access_token={var}"),
|
||||
("parse",["tracks","items",0,"album","images",0,"url"])
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def api_request_artist(artist):
|
||||
for api in apis_artists:
|
||||
if True:
|
||||
log("API: " + api["name"] + "; Image request: " + artist,module="external")
|
||||
try:
|
||||
artiststring = urllib.parse.quote(artist)
|
||||
var = artiststring
|
||||
for step in api["steps"]:
|
||||
if step[0] == "get":
|
||||
response = urllib.request.urlopen(step[1].format(artiststring=artiststring,var=var))
|
||||
var = json.loads(response.read())
|
||||
elif step[0] == "post":
|
||||
keys = {
|
||||
"url":step[1].format(artiststring=artiststring,var=var),
|
||||
"method":"POST",
|
||||
"headers":step[2],
|
||||
"data":bytes(urllib.parse.urlencode(step[3]),encoding="utf-8")
|
||||
}
|
||||
req = urllib.request.Request(**keys)
|
||||
response = urllib.request.urlopen(req)
|
||||
var = json.loads(response.read())
|
||||
elif step[0] == "parse":
|
||||
for node in step[1]:
|
||||
var = var[node]
|
||||
assert isinstance(var,str) and var != ""
|
||||
except:
|
||||
continue
|
||||
|
||||
return var
|
||||
else:
|
||||
pass
|
||||
return None
|
||||
|
||||
def api_request_track(track):
|
||||
artists, title = track
|
||||
for api in apis_tracks:
|
||||
if True:
|
||||
log("API: " + api["name"] + "; Image request: " + "/".join(artists) + " - " + title,module="external")
|
||||
try:
|
||||
artiststring = urllib.parse.quote(", ".join(artists))
|
||||
titlestring = urllib.parse.quote(title)
|
||||
var = artiststring + titlestring
|
||||
for step in api["steps"]:
|
||||
if step[0] == "get":
|
||||
response = urllib.request.urlopen(step[1].format(artiststring=artiststring,titlestring=titlestring,var=var))
|
||||
var = json.loads(response.read())
|
||||
elif step[0] == "post":
|
||||
keys = {
|
||||
"url":step[1].format(artiststring=artiststring,titlestring=titlestring,var=var),
|
||||
"method":"POST",
|
||||
"headers":step[2],
|
||||
"data":bytes(urllib.parse.urlencode(step[3]),encoding="utf-8")
|
||||
}
|
||||
req = urllib.request.Request(**keys)
|
||||
response = urllib.request.urlopen(req)
|
||||
var = json.loads(response.read())
|
||||
elif step[0] == "parse":
|
||||
for node in step[1]:
|
||||
var = var[node]
|
||||
assert isinstance(var,str) and var != ""
|
||||
except:
|
||||
if len(artists) != 1:
|
||||
# try the same track with every single artist
|
||||
for a in artists:
|
||||
result = api_request_track(([a],title))
|
||||
if result is not None:
|
||||
return result
|
||||
continue
|
||||
|
||||
return var
|
||||
else:
|
||||
pass
|
||||
|
||||
return None
|
|
@ -1,46 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
from cleanup import CleanerAgent
|
||||
from doreah.logging import log
|
||||
import difflib
|
||||
|
||||
wendigo = CleanerAgent()
|
||||
|
||||
exp = r"([0-9]*)(\t+)([^\t]+?)(\t+)([^\t]+)(\t*)([^\t]*)\n"
|
||||
|
||||
for fn in os.listdir("scrobbles/"):
|
||||
if fn.endswith(".tsv"):
|
||||
f = open("scrobbles/" + fn)
|
||||
fnew = open("scrobbles/" + fn + "_new","w")
|
||||
for l in f:
|
||||
|
||||
a,t = re.sub(exp,r"\3",l), re.sub(exp,r"\5",l)
|
||||
r1,r2,r3 = re.sub(exp,r"\1\2",l),re.sub(exp,r"\4",l),re.sub(exp,r"\6\7",l)
|
||||
|
||||
a = a.replace("␟",";")
|
||||
|
||||
(al,t) = wendigo.fullclean(a,t)
|
||||
a = "␟".join(al)
|
||||
fnew.write(r1 + a + r2 + t + r3 + "\n")
|
||||
|
||||
#print("Artists: " + a)
|
||||
#print("Title: " + t)
|
||||
#print("1: " + r1)
|
||||
#print("2: " + r2)
|
||||
#print("3: " + r3)
|
||||
|
||||
f.close()
|
||||
fnew.close()
|
||||
|
||||
#os.system("diff " + "scrobbles/" + fn + "_new" + " " + "scrobbles/" + fn)
|
||||
with open("scrobbles/" + fn + "_new","r") as newfile:
|
||||
with open("scrobbles/" + fn,"r") as oldfile:
|
||||
diff = difflib.unified_diff(oldfile.read().split("\n"),newfile.read().split("\n"),lineterm="")
|
||||
diff = list(diff)[2:]
|
||||
log("Diff for scrobbles/" + fn + "".join("\n\t" + d for d in diff),module="fixer")
|
||||
|
||||
os.rename("scrobbles/" + fn + "_new","scrobbles/" + fn)
|
||||
|
||||
checkfile = open("scrobbles/" + fn + ".rulestate","w")
|
||||
checkfile.write(wendigo.checksums)
|
||||
checkfile.close()
|
|
@ -1,133 +0,0 @@
|
|||
import urllib
|
||||
from bottle import FormsDict
|
||||
import datetime
|
||||
from urihandler import compose_querystring
|
||||
|
||||
|
||||
# returns the proper column(s) for an artist or track
|
||||
def entity_column(element,counting=[],image=None):
|
||||
|
||||
html = ""
|
||||
|
||||
if image is not None:
|
||||
html += """<td class='icon'><div style="background-image:url('""" + image + """')"></div></td>"""
|
||||
|
||||
if "artists" in element:
|
||||
# track
|
||||
# html += "<td class='artists'>" + html_links(element["artists"]) + "</td>"
|
||||
# html += "<td class='title'>" + html_link(element) + "</td>"
|
||||
html += "<td class='track'><span class='artist_in_trackcolumn'>" + html_links(element["artists"]) + "</span> – " + html_link(element) + "</td>"
|
||||
else:
|
||||
# artist
|
||||
html += "<td class='artist'>" + html_link(element)
|
||||
if (counting != []):
|
||||
html += " <span class='extra'>incl. " + html_links(counting) + "</span>"
|
||||
html += "</td>"
|
||||
|
||||
return html
|
||||
|
||||
def uri_query(entity):
|
||||
if "artists" in entity:
|
||||
#track
|
||||
return "title=" + urllib.parse.quote(entity["title"]) \
|
||||
+ "&" + "&".join(["artist=" + urllib.parse.quote(a) for a in entity["artists"]])
|
||||
|
||||
else:
|
||||
#artist
|
||||
return "artist=" + urllib.parse.quote(entity)
|
||||
|
||||
# returns the address of the track / artist page
|
||||
def link_address(entity):
|
||||
if "artists" in entity:
|
||||
#track
|
||||
return "/track?" + uri_query(entity)
|
||||
else:
|
||||
#artist
|
||||
return "/artist?" + uri_query(entity)
|
||||
|
||||
#returns linked name
|
||||
def html_link(entity):
|
||||
if "artists" in entity:
|
||||
#track
|
||||
name = entity["title"]
|
||||
else:
|
||||
#artist
|
||||
name = entity
|
||||
return "<a href='" + link_address(entity) + "'>" + name + "</a>"
|
||||
|
||||
def html_links(entities):
|
||||
return ", ".join([html_link(e) for e in entities])
|
||||
|
||||
# DEPRECATED
|
||||
def artistLink(name):
|
||||
return html_link(name)
|
||||
#return "<a href='/artist?artist=" + urllib.parse.quote(name) + "'>" + name + "</a>"
|
||||
|
||||
# DEPRECATED
|
||||
def artistLinks(artists):
|
||||
return ", ".join([artistLink(a) for a in artists])
|
||||
|
||||
#def trackLink(artists,title):
|
||||
# DEPRECATED
|
||||
def trackLink(track):
|
||||
return html_link(track)
|
||||
#artists,title = track["artists"],track["title"]
|
||||
#return "<a href='/track?title=" + urllib.parse.quote(title) + "&" + "&".join(["artist=" + urllib.parse.quote(a) for a in artists]) + "'>" + title + "</a>"
|
||||
|
||||
#def scrobblesTrackLink(artists,title,timekeys,amount=None,pixels=None):
|
||||
def scrobblesTrackLink(track,timekeys,amount=None,percent=None):
|
||||
artists,title = track["artists"],track["title"]
|
||||
inner = str(amount) if amount is not None else "<div style='width:" + str(percent) + "%;'></div>"
|
||||
return "<a href='/scrobbles?" + uri_query(track) + "&" + compose_querystring(timekeys) + "'>" + inner + "</a>"
|
||||
|
||||
def scrobblesArtistLink(artist,timekeys,amount=None,percent=None,associated=False):
|
||||
inner = str(amount) if amount is not None else "<div style='width:" + str(percent) + "%;'></div>"
|
||||
askey = "&associated" if associated else ""
|
||||
return "<a href='/scrobbles?" + uri_query(artist) + "&" + compose_querystring(timekeys) + askey + "'>" + inner + "</a>"
|
||||
|
||||
def scrobblesLink(timekeys,amount=None,percent=None,artist=None,track=None,associated=False):
|
||||
if track is not None: return scrobblesTrackLink(track,timekeys,amount,percent)
|
||||
if artist is not None: return scrobblesArtistLink(artist,timekeys,amount,percent,associated)
|
||||
inner = str(amount) if amount is not None else "<div style='width:" + str(percent) + "%;'></div>"
|
||||
return "<a href='/scrobbles?" + compose_querystring(timekeys) + "'>" + inner + "</a>"
|
||||
|
||||
|
||||
|
||||
def rankTrackLink(track,timekeys,rank=None,percent=None,medal=None):
|
||||
cl = ""
|
||||
if medal == 1: cl = "class='gold'"
|
||||
if medal == 2: cl = "class='silver'"
|
||||
if medal == 3: cl = "class='bronze'"
|
||||
inner = str(rank) if rank is not None else "<div " + cl + " style='width:" + str(percent) + "%;'></div>"
|
||||
|
||||
return "<a href='/charts_tracks?" + compose_querystring(timekeys) + "'>" + inner + "</a>"
|
||||
|
||||
def rankArtistLink(artist,timekeys,rank=None,percent=None,medal=None):
|
||||
cl = ""
|
||||
if medal == 1: cl = "class='gold'"
|
||||
if medal == 2: cl = "class='silver'"
|
||||
if medal == 3: cl = "class='bronze'"
|
||||
inner = str(rank) if rank is not None else "<div " + cl + " style='width:" + str(percent) + "%;'></div>"
|
||||
return "<a href='/charts_artists?" + compose_querystring(timekeys) + "'>" + inner + "</a>"
|
||||
|
||||
def rankLink(timekeys,rank=None,percent=None,artist=None,track=None,medal=None):
|
||||
if track is not None: return rankTrackLink(track,timekeys,rank,percent,medal)
|
||||
if artist is not None: return rankArtistLink(artist,timekeys,rank,percent,medal)
|
||||
|
||||
|
||||
|
||||
# limit a multidict to only the specified keys
|
||||
# would be a simple constructor expression, but multidicts apparently don't let me do that
|
||||
def pickKeys(d,*keys):
|
||||
if isinstance(d,dict):
|
||||
return {k:d.get(k) for k in d if k in keys}
|
||||
else:
|
||||
# create a normal dictionary of lists
|
||||
newd = {k:d.getall(k) for k in d if k in keys}
|
||||
# one by one add the list entries to the formsdict
|
||||
finald = FormsDict()
|
||||
for k in newd:
|
||||
for v in newd.get(k):
|
||||
finald.append(k,v)
|
||||
|
||||
return finald
|
739
htmlmodules.py
739
htmlmodules.py
|
@ -1,739 +0,0 @@
|
|||
from htmlgenerators import *
|
||||
import database
|
||||
from utilities import getArtistImage, getTrackImage
|
||||
from malojatime import *
|
||||
from urihandler import compose_querystring, internal_to_uri, uri_to_internal
|
||||
import urllib
|
||||
import datetime
|
||||
import math
|
||||
|
||||
|
||||
#def getpictures(ls,result,tracks=False):
|
||||
# from utilities import getArtistsInfo, getTracksInfo
|
||||
# if tracks:
|
||||
# for element in getTracksInfo(ls):
|
||||
# result.append(element.get("image"))
|
||||
# else:
|
||||
# for element in getArtistsInfo(ls):
|
||||
# result.append(element.get("image"))
|
||||
|
||||
|
||||
#max_ indicates that no pagination should occur (because this is not the primary module)
|
||||
def module_scrobblelist(page=0,perpage=100,max_=None,pictures=False,shortTimeDesc=False,earlystop=False,**kwargs):
|
||||
|
||||
kwargs_filter = pickKeys(kwargs,"artist","track","associated")
|
||||
kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
|
||||
|
||||
if max_ is not None: perpage,page=max_,0
|
||||
|
||||
firstindex = page * perpage
|
||||
lastindex = firstindex + perpage
|
||||
|
||||
# if earlystop, we don't care about the actual amount and only request as many from the db
|
||||
# without, we request everything and filter on site
|
||||
maxkey = {"max_":lastindex} if earlystop else {}
|
||||
scrobbles = database.get_scrobbles(**kwargs_time,**kwargs_filter,**maxkey)
|
||||
if pictures:
|
||||
scrobbleswithpictures = [""] * firstindex + scrobbles[firstindex:lastindex]
|
||||
#scrobbleimages = [e.get("image") for e in getTracksInfo(scrobbleswithpictures)] #will still work with scrobble objects as they are a technically a subset of track objects
|
||||
#scrobbleimages = ["/image?title=" + urllib.parse.quote(t["title"]) + "&" + "&".join(["artist=" + urllib.parse.quote(a) for a in t["artists"]]) for t in scrobbleswithpictures]
|
||||
scrobbleimages = [getTrackImage(t["artists"],t["title"],fast=True) for t in scrobbleswithpictures]
|
||||
|
||||
pages = math.ceil(len(scrobbles) / perpage)
|
||||
|
||||
representative = scrobbles[0] if len(scrobbles) is not 0 else None
|
||||
|
||||
# build list
|
||||
i = 0
|
||||
html = "<table class='list'>"
|
||||
for s in scrobbles:
|
||||
if i<firstindex:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
html += "<tr>"
|
||||
html += "<td class='time'>" + timestamp_desc(s["time"],short=shortTimeDesc) + "</td>"
|
||||
if pictures:
|
||||
img = scrobbleimages[i]
|
||||
else: img = None
|
||||
html += entity_column(s,image=img)
|
||||
html += "</tr>"
|
||||
|
||||
i += 1
|
||||
if i>=lastindex:
|
||||
break
|
||||
|
||||
|
||||
html += "</table>"
|
||||
|
||||
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
|
||||
|
||||
return (html,len(scrobbles),representative)
|
||||
|
||||
|
||||
def module_pulse(page=0,perpage=100,max_=None,**kwargs):
|
||||
|
||||
from doreah.timing import clock, clockp
|
||||
|
||||
kwargs_filter = pickKeys(kwargs,"artist","track","associated")
|
||||
kwargs_time = pickKeys(kwargs,"since","to","within","timerange","step","stepn","trail")
|
||||
|
||||
if max_ is not None: perpage,page=max_,0
|
||||
|
||||
firstindex = page * perpage
|
||||
lastindex = firstindex + perpage
|
||||
|
||||
|
||||
ranges = database.get_pulse(**kwargs_time,**kwargs_filter)
|
||||
|
||||
pages = math.ceil(len(ranges) / perpage)
|
||||
|
||||
ranges = ranges[firstindex:lastindex]
|
||||
|
||||
# if time range not explicitly specified, only show from first appearance
|
||||
# if "since" not in kwargs:
|
||||
# while ranges[0]["scrobbles"] == 0:
|
||||
# del ranges[0]
|
||||
|
||||
maxbar = max([t["scrobbles"] for t in ranges])
|
||||
maxbar = max(maxbar,1)
|
||||
|
||||
#build list
|
||||
html = "<table class='list'>"
|
||||
for t in ranges:
|
||||
range = t["range"]
|
||||
html += "<tr>"
|
||||
html += "<td>" + range.desc() + "</td>"
|
||||
html += "<td class='amount'>" + scrobblesLink(range.urikeys(),amount=t["scrobbles"],**kwargs_filter) + "</td>"
|
||||
html += "<td class='bar'>" + scrobblesLink(range.urikeys(),percent=t["scrobbles"]*100/maxbar,**kwargs_filter) + "</td>"
|
||||
html += "</tr>"
|
||||
html += "</table>"
|
||||
|
||||
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
|
||||
def module_performance(page=0,perpage=100,max_=None,**kwargs):
|
||||
|
||||
kwargs_filter = pickKeys(kwargs,"artist","track")
|
||||
kwargs_time = pickKeys(kwargs,"since","to","within","timerange","step","stepn","trail")
|
||||
|
||||
if max_ is not None: perpage,page=max_,0
|
||||
|
||||
firstindex = page * perpage
|
||||
lastindex = firstindex + perpage
|
||||
|
||||
ranges = database.get_performance(**kwargs_time,**kwargs_filter)
|
||||
|
||||
pages = math.ceil(len(ranges) / perpage)
|
||||
|
||||
ranges = ranges[firstindex:lastindex]
|
||||
|
||||
# if time range not explicitly specified, only show from first appearance
|
||||
# if "since" not in kwargs:
|
||||
# while ranges[0]["scrobbles"] == 0:
|
||||
# del ranges[0]
|
||||
|
||||
|
||||
minrank = 80
|
||||
for t in ranges:
|
||||
if t["rank"] is not None and t["rank"]+20 > minrank: minrank = t["rank"]+20
|
||||
|
||||
#build list
|
||||
html = "<table class='list'>"
|
||||
for t in ranges:
|
||||
range = t["range"]
|
||||
html += "<tr>"
|
||||
html += "<td>" + range.desc() + "</td>"
|
||||
html += "<td class='rank'>" + ("#" + str(t["rank"]) if t["rank"] is not None else "No scrobbles") + "</td>"
|
||||
prct = (minrank+1-t["rank"])*100/minrank if t["rank"] is not None else 0
|
||||
html += "<td class='chart'>" + rankLink(range.urikeys(),percent=prct,**kwargs_filter,medal=t["rank"]) + "</td>"
|
||||
html += "</tr>"
|
||||
html += "</table>"
|
||||
|
||||
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
|
||||
def module_trackcharts(page=0,perpage=100,max_=None,**kwargs):
|
||||
|
||||
kwargs_filter = pickKeys(kwargs,"artist","associated")
|
||||
kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
|
||||
|
||||
if max_ is not None: perpage,page=max_,0
|
||||
|
||||
firstindex = page * perpage
|
||||
lastindex = firstindex + perpage
|
||||
|
||||
tracks = database.get_charts_tracks(**kwargs_filter,**kwargs_time)
|
||||
|
||||
pages = math.ceil(len(tracks) / perpage)
|
||||
|
||||
# last time range (to compare)
|
||||
try:
|
||||
trackslast = database.get_charts_tracks(**kwargs_filter,timerange=kwargs_time["timerange"].next(step=-1))
|
||||
# create rank association
|
||||
lastrank = {}
|
||||
for tl in trackslast:
|
||||
lastrank[(*tl["track"]["artists"],tl["track"]["title"])] = tl["rank"]
|
||||
for t in tracks:
|
||||
try:
|
||||
t["delta"] = lastrank[(*t["track"]["artists"],t["track"]["title"])] - t["rank"]
|
||||
except:
|
||||
t["delta"] = math.inf
|
||||
except:
|
||||
pass
|
||||
|
||||
if tracks != []:
|
||||
maxbar = tracks[0]["scrobbles"]
|
||||
representative = tracks[0]["track"]
|
||||
else:
|
||||
representative = None
|
||||
|
||||
|
||||
i = 0
|
||||
html = "<table class='list'>"
|
||||
for e in tracks:
|
||||
if i<firstindex:
|
||||
i += 1
|
||||
continue
|
||||
i += 1
|
||||
if i>lastindex:
|
||||
break
|
||||
html += "<tr>"
|
||||
# rank
|
||||
if i == firstindex+1 or e["scrobbles"] < prev["scrobbles"]:
|
||||
html += "<td class='rank'>#" + str(e["rank"]) + "</td>"
|
||||
else:
|
||||
html += "<td class='rank'></td>"
|
||||
# rank change
|
||||
if e.get("delta") is None:
|
||||
pass
|
||||
elif e["delta"] is math.inf:
|
||||
html += "<td class='rankup' title='New'>🆕</td>"
|
||||
elif e["delta"] > 0:
|
||||
html += "<td class='rankup' title='up from #" + str(e["rank"]+e["delta"]) + "'>↗</td>"
|
||||
elif e["delta"] < 0:
|
||||
html += "<td class='rankdown' title='down from #" + str(e["rank"]+e["delta"]) + "'>↘</td>"
|
||||
else:
|
||||
html += "<td class='ranksame' title='Unchanged'>➡</td>"
|
||||
# track
|
||||
html += entity_column(e["track"])
|
||||
# scrobbles
|
||||
html += "<td class='amount'>" + scrobblesTrackLink(e["track"],internal_to_uri(kwargs_time),amount=e["scrobbles"]) + "</td>"
|
||||
html += "<td class='bar'>" + scrobblesTrackLink(e["track"],internal_to_uri(kwargs_time),percent=e["scrobbles"]*100/maxbar) + "</td>"
|
||||
html += "</tr>"
|
||||
prev = e
|
||||
html += "</table>"
|
||||
|
||||
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
|
||||
|
||||
return (html,representative)
|
||||
|
||||
|
||||
def module_artistcharts(page=0,perpage=100,max_=None,**kwargs):
|
||||
|
||||
kwargs_filter = pickKeys(kwargs,"associated") #not used right now
|
||||
kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
|
||||
|
||||
if max_ is not None: perpage,page=max_,0
|
||||
|
||||
firstindex = page * perpage
|
||||
lastindex = firstindex + perpage
|
||||
|
||||
artists = database.get_charts_artists(**kwargs_filter,**kwargs_time)
|
||||
|
||||
pages = math.ceil(len(artists) / perpage)
|
||||
|
||||
|
||||
# last time range (to compare)
|
||||
try:
|
||||
#from malojatime import _get_next
|
||||
artistslast = database.get_charts_artists(**kwargs_filter,timerange=kwargs_time["timerange"].next(step=-1))
|
||||
# create rank association
|
||||
lastrank = {}
|
||||
for al in artistslast:
|
||||
lastrank[al["artist"]] = al["rank"]
|
||||
for a in artists:
|
||||
try:
|
||||
a["delta"] = lastrank[a["artist"]] - a["rank"]
|
||||
except:
|
||||
a["delta"] = math.inf
|
||||
except:
|
||||
pass
|
||||
|
||||
if artists != []:
|
||||
maxbar = artists[0]["scrobbles"]
|
||||
representative = artists[0]["artist"]
|
||||
else:
|
||||
representative = None
|
||||
|
||||
i = 0
|
||||
html = "<table class='list'>"
|
||||
for e in artists:
|
||||
if i<firstindex:
|
||||
i += 1
|
||||
continue
|
||||
i += 1
|
||||
if i>lastindex:
|
||||
break
|
||||
html += "<tr>"
|
||||
# rank
|
||||
if i == firstindex+1 or e["scrobbles"] < prev["scrobbles"]:
|
||||
html += "<td class='rank'>#" + str(e["rank"]) + "</td>"
|
||||
else:
|
||||
html += "<td class='rank'></td>"
|
||||
# rank change
|
||||
#if "within" not in kwargs_time: pass
|
||||
if e.get("delta") is None:
|
||||
pass
|
||||
elif e["delta"] is math.inf:
|
||||
html += "<td class='rankup' title='New'>🆕</td>"
|
||||
elif e["delta"] > 0:
|
||||
html += "<td class='rankup' title='up from #" + str(e["rank"]+e["delta"]) + "'>↗</td>"
|
||||
elif e["delta"] < 0:
|
||||
html += "<td class='rankdown' title='down from #" + str(e["rank"]+e["delta"]) + "'>↘</td>"
|
||||
else:
|
||||
html += "<td class='ranksame' title='Unchanged'>➡</td>"
|
||||
# artist
|
||||
html += entity_column(e["artist"],counting=e["counting"])
|
||||
# scrobbles
|
||||
html += "<td class='amount'>" + scrobblesArtistLink(e["artist"],internal_to_uri(kwargs_time),amount=e["scrobbles"],associated=True) + "</td>"
|
||||
html += "<td class='bar'>" + scrobblesArtistLink(e["artist"],internal_to_uri(kwargs_time),percent=e["scrobbles"]*100/maxbar,associated=True) + "</td>"
|
||||
html += "</tr>"
|
||||
prev = e
|
||||
|
||||
html += "</table>"
|
||||
|
||||
if max_ is None: html += module_paginate(page=page,pages=pages,perpage=perpage,**kwargs)
|
||||
|
||||
return (html, representative)
|
||||
|
||||
|
||||
|
||||
def module_toptracks(pictures=True,**kwargs):
|
||||
|
||||
kwargs_filter = pickKeys(kwargs,"artist","associated")
|
||||
kwargs_time = pickKeys(kwargs,"timerange","since","to","within","step","stepn","trail")
|
||||
|
||||
tracks = database.get_top_tracks(**kwargs_filter,**kwargs_time)
|
||||
|
||||
if tracks != []:
|
||||
maxbar = max(t["scrobbles"] for t in tracks)
|
||||
|
||||
|
||||
# track with most #1 positions
|
||||
max_appear = 0
|
||||
representatives = list(t["track"] for t in tracks if t["track"] is not None)
|
||||
for t in representatives:
|
||||
max_appear = max(max_appear,representatives.count(t))
|
||||
#representatives.sort(key=lambda reftrack:len([t for t in tracks if t["track"] == reftrack["track"] and t["track"] is not None]))
|
||||
representatives = [t for t in tracks if representatives.count(t["track"]) == max_appear]
|
||||
# of these, track with highest scrobbles in its #1 range
|
||||
representatives.sort(key=lambda t: t["scrobbles"])
|
||||
representative = representatives[-1]["track"]
|
||||
else:
|
||||
representative = None
|
||||
|
||||
|
||||
i = 0
|
||||
html = "<table class='list'>"
|
||||
for e in tracks:
|
||||
|
||||
#fromstr = "/".join([str(p) for p in e["from"]])
|
||||
#tostr = "/".join([str(p) for p in e["to"]])
|
||||
range = e["range"]
|
||||
|
||||
i += 1
|
||||
html += "<tr>"
|
||||
|
||||
|
||||
html += "<td>" + range.desc() + "</td>"
|
||||
if e["track"] is None:
|
||||
if pictures:
|
||||
html += "<td><div></div></td>"
|
||||
html += "<td class='stats'>" + "No scrobbles" + "</td>"
|
||||
#html += "<td>" + "" + "</td>"
|
||||
html += "<td class='amount'>" + "0" + "</td>"
|
||||
html += "<td class='bar'>" + "" + "</td>"
|
||||
else:
|
||||
if pictures:
|
||||
img = getTrackImage(e["track"]["artists"],e["track"]["title"],fast=True)
|
||||
else: img = None
|
||||
html += entity_column(e["track"],image=img)
|
||||
html += "<td class='amount'>" + scrobblesTrackLink(e["track"],range.urikeys(),amount=e["scrobbles"]) + "</td>"
|
||||
html += "<td class='bar'>" + scrobblesTrackLink(e["track"],range.urikeys(),percent=e["scrobbles"]*100/maxbar) + "</td>"
|
||||
html += "</tr>"
|
||||
prev = e
|
||||
html += "</table>"
|
||||
|
||||
return (html,representative)
|
||||
|
||||
def module_topartists(pictures=True,**kwargs):
|
||||
|
||||
kwargs_time = pickKeys(kwargs,"timerange","since","to","within","step","stepn","trail")
|
||||
|
||||
artists = database.get_top_artists(**kwargs_time)
|
||||
|
||||
if artists != []:
|
||||
maxbar = max(a["scrobbles"] for a in artists)
|
||||
|
||||
# artists with most #1 positions
|
||||
max_appear = 0
|
||||
representatives = list(a["artist"] for a in artists if a["artist"] is not None)
|
||||
for a in representatives:
|
||||
max_appear = max(max_appear,representatives.count(a))
|
||||
representatives = [a for a in artists if representatives.count(a["artist"]) == max_appear]
|
||||
# of these, artist with highest scrobbles in their #1 range
|
||||
representatives.sort(key=lambda a: a["scrobbles"])
|
||||
|
||||
representative = representatives[-1]["artist"]
|
||||
else:
|
||||
representative = None
|
||||
|
||||
|
||||
i = 0
|
||||
html = "<table class='list'>"
|
||||
for e in artists:
|
||||
|
||||
#fromstr = "/".join([str(p) for p in e["from"]])
|
||||
#tostr = "/".join([str(p) for p in e["to"]])
|
||||
range = e["range"]
|
||||
|
||||
i += 1
|
||||
html += "<tr>"
|
||||
|
||||
|
||||
html += "<td>" + range.desc() + "</td>"
|
||||
|
||||
if e["artist"] is None:
|
||||
if pictures:
|
||||
html += "<td><div></div></td>"
|
||||
html += "<td class='stats'>" + "No scrobbles" + "</td>"
|
||||
html += "<td class='amount'>" + "0" + "</td>"
|
||||
html += "<td class='bar'>" + "" + "</td>"
|
||||
else:
|
||||
if pictures:
|
||||
img = getArtistImage(e["artist"],fast=True)
|
||||
else: img = None
|
||||
html += entity_column(e["artist"],image=img)
|
||||
html += "<td class='amount'>" + scrobblesArtistLink(e["artist"],range.urikeys(),amount=e["scrobbles"],associated=True) + "</td>"
|
||||
html += "<td class='bar'>" + scrobblesArtistLink(e["artist"],range.urikeys(),percent=e["scrobbles"]*100/maxbar,associated=True) + "</td>"
|
||||
html += "</tr>"
|
||||
prev = e
|
||||
html += "</table>"
|
||||
|
||||
return (html,representative)
|
||||
|
||||
|
||||
def module_artistcharts_tiles(**kwargs):
|
||||
|
||||
kwargs_filter = pickKeys(kwargs,"associated") #not used right now
|
||||
kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
|
||||
|
||||
artists = database.get_charts_artists(**kwargs_filter,**kwargs_time)[:14]
|
||||
while len(artists)<14: artists.append(None)
|
||||
|
||||
i = 1
|
||||
|
||||
bigpart = [0,1,2,6,15]
|
||||
smallpart = [0,1,2,4,6,9,12,15]
|
||||
#rnk = (0,0) #temporary store so entries with the same scrobble amount get the same rank
|
||||
|
||||
html = """<table class="tiles_top"><tr>"""
|
||||
|
||||
for e in artists:
|
||||
|
||||
|
||||
if i in bigpart:
|
||||
n = bigpart.index(i)
|
||||
html += """<td><table class="tiles_""" + str(n) + """x""" + str(n) + """ tiles_sub">"""
|
||||
|
||||
if i in smallpart:
|
||||
html += "<tr>"
|
||||
|
||||
|
||||
if e is not None:
|
||||
html += "<td onclick='window.location.href=\"" \
|
||||
+ link_address(e["artist"]) \
|
||||
+ "\"' style='cursor:pointer;background-image:url(\"" + getArtistImage(e["artist"],fast=True) + "\");'>" \
|
||||
+ "<span class='stats'>" + "#" + str(e["rank"]) + "</span> <span>" + html_link(e["artist"]) + "</span></td>"
|
||||
else:
|
||||
html += "<td><span class='stats'></span> <span></span></td>"
|
||||
|
||||
i += 1
|
||||
|
||||
if i in smallpart:
|
||||
html += "</tr>"
|
||||
|
||||
if i in bigpart:
|
||||
html += "</table></td>"
|
||||
|
||||
html += """</tr></table>"""
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def module_trackcharts_tiles(**kwargs):
|
||||
|
||||
kwargs_filter = pickKeys(kwargs,"artist","associated")
|
||||
kwargs_time = pickKeys(kwargs,"timerange","since","to","within")
|
||||
|
||||
tracks = database.get_charts_tracks(**kwargs_filter,**kwargs_time)[:14]
|
||||
while len(tracks)<14: tracks.append(None) #{"track":{"title":"","artists":[]}}
|
||||
|
||||
i = 1
|
||||
|
||||
bigpart = [0,1,2,6,15]
|
||||
smallpart = [0,1,2,4,6,9,12,15]
|
||||
#rnk = (0,0) #temporary store so entries with the same scrobble amount get the same rank
|
||||
|
||||
|
||||
html = """<table class="tiles_top"><tr>"""
|
||||
|
||||
for e in tracks:
|
||||
|
||||
|
||||
if i in bigpart:
|
||||
n = bigpart.index(i)
|
||||
html += """<td><table class="tiles_""" + str(n) + """x""" + str(n) + """ tiles_sub">"""
|
||||
|
||||
if i in smallpart:
|
||||
html += "<tr>"
|
||||
|
||||
|
||||
if e is not None:
|
||||
html += "<td onclick='window.location.href=\"" \
|
||||
+ link_address(e["track"]) \
|
||||
+ "\"' style='cursor:pointer;background-image:url(\"" + getTrackImage(e["track"]["artists"],e["track"]["title"],fast=True) + "\");'>" \
|
||||
+ "<span class='stats'>" + "#" + str(e["rank"]) + "</span> <span>" + html_link(e["track"]) + "</span></td>"
|
||||
else:
|
||||
html += "<td><span class='stats'></span> <span></span></td>"
|
||||
|
||||
i += 1
|
||||
|
||||
if i in smallpart:
|
||||
html += "</tr>"
|
||||
|
||||
if i in bigpart:
|
||||
html += "</table></td>"
|
||||
|
||||
html += """</tr></table>"""
|
||||
|
||||
return html
|
||||
|
||||
|
||||
|
||||
def module_paginate(page,pages,perpage,**keys):
|
||||
|
||||
unchangedkeys = internal_to_uri({**keys,"perpage":perpage})
|
||||
|
||||
html = "<div class='paginate'>"
|
||||
|
||||
if page > 1:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":0})) + "'><span class='stat_selector'>" + "1" + "</span></a>"
|
||||
html += " | "
|
||||
|
||||
if page > 2:
|
||||
html += " ... | "
|
||||
|
||||
if page > 0:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":page-1})) + "'><span class='stat_selector'>" + str(page) + "</span></a>"
|
||||
html += " « "
|
||||
|
||||
html += "<span style='opacity:0.5;' class='stat_selector'>" + str(page+1) + "</span>"
|
||||
|
||||
if page < pages-1:
|
||||
html += " » "
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":page+1})) + "'><span class='stat_selector'>" + str(page+2) + "</span></a>"
|
||||
|
||||
if page < pages-3:
|
||||
html += " | ... "
|
||||
|
||||
if page < pages-2:
|
||||
html += " | "
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"page":pages-1})) + "'><span class='stat_selector'>" + str(pages) + "</span></a>"
|
||||
|
||||
|
||||
html += "</div>"
|
||||
|
||||
return html
|
||||
|
||||
|
||||
|
||||
# THIS FUNCTION USES THE ORIGINAL URI KEYS!!!
|
||||
def module_filterselection(keys,time=True,delimit=False):
|
||||
|
||||
filterkeys, timekeys, delimitkeys, extrakeys = uri_to_internal(keys)
|
||||
|
||||
# drop keys that are not relevant so they don't clutter the URI
|
||||
if not time: timekeys = {}
|
||||
if not delimit: delimitkeys = {}
|
||||
|
||||
html = ""
|
||||
|
||||
|
||||
if time:
|
||||
# all other keys that will not be changed by clicking another filter
|
||||
#keystr = "?" + compose_querystring(keys,exclude=["since","to","in"])
|
||||
unchangedkeys = internal_to_uri({**filterkeys,**delimitkeys,**extrakeys})
|
||||
|
||||
|
||||
# wonky selector for precise date range
|
||||
|
||||
# fromdate = start_of_scrobbling()
|
||||
# todate = end_of_scrobbling()
|
||||
# if keys.get("since") is not None: fromdate = keys.get("since")
|
||||
# if keys.get("to") is not None: todate = keys.get("to")
|
||||
# if keys.get("in") is not None: fromdate, todate = keys.get("in"), keys.get("in")
|
||||
# fromdate = time_fix(fromdate)
|
||||
# todate = time_fix(todate)
|
||||
# fromdate, todate = time_pad(fromdate,todate,full=True)
|
||||
# fromdate = [str(e) if e>9 else "0" + str(e) for e in fromdate]
|
||||
# todate = [str(e) if e>9 else "0" + str(e) for e in todate]
|
||||
#
|
||||
# html += "<div>"
|
||||
# html += "from <input id='dateselect_from' onchange='datechange()' type='date' value='" + "-".join(fromdate) + "'/> "
|
||||
# html += "to <input id='dateselect_to' onchange='datechange()' type='date' value='" + "-".join(todate) + "'/>"
|
||||
# html += "</div>"
|
||||
|
||||
from malojatime import today, thisweek, thismonth, thisyear
|
||||
|
||||
### temp!!! this will not allow weekly rank changes
|
||||
# weekday = ((now.isoweekday()) % 7)
|
||||
# weekbegin = now - datetime.timedelta(days=weekday)
|
||||
# weekend = weekbegin + datetime.timedelta(days=6)
|
||||
# weekbegin = [weekbegin.year,weekbegin.month,weekbegin.day]
|
||||
# weekend = [weekend.year,weekend.month,weekend.day]
|
||||
# weekbeginstr = "/".join((str(num) for num in weekbegin))
|
||||
# weekendstr = "/".join((str(num) for num in weekend))
|
||||
|
||||
|
||||
|
||||
# relative to current range
|
||||
|
||||
html += "<div>"
|
||||
# if timekeys.get("timerange").next(-1) is not None:
|
||||
# html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":timekeys.get("timerange").next(-1)})) + "'><span class='stat_selector'>«</span></a>"
|
||||
# if timekeys.get("timerange").next(-1) is not None or timekeys.get("timerange").next(1) is not None:
|
||||
# html += " " + timekeys.get("timerange").desc() + " "
|
||||
# if timekeys.get("timerange").next(1) is not None:
|
||||
# html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":timekeys.get("timerange").next(1)})) + "'><span class='stat_selector'>»</span></a>"
|
||||
|
||||
if timekeys.get("timerange").next(-1) is not None:
|
||||
prevrange = timekeys.get("timerange").next(-1)
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":prevrange})) + "'><span class='stat_selector'>" + prevrange.desc() + "</span></a>"
|
||||
html += " « "
|
||||
if timekeys.get("timerange").next(-1) is not None or timekeys.get("timerange").next(1) is not None:
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>" + timekeys.get("timerange").desc() + "</span>"
|
||||
if timekeys.get("timerange").next(1) is not None:
|
||||
html += " » "
|
||||
nextrange = timekeys.get("timerange").next(1)
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,internal_to_uri({"timerange":nextrange})) + "'><span class='stat_selector'>" + nextrange.desc() + "</span></a>"
|
||||
|
||||
html += "</div>"
|
||||
|
||||
|
||||
# predefined ranges
|
||||
|
||||
html += "<div>"
|
||||
if timekeys.get("timerange") == today():
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>Today</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"today"}) + "'><span class='stat_selector'>Today</span></a>"
|
||||
html += " | "
|
||||
|
||||
if timekeys.get("timerange") == thisweek():
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>This Week</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"week"}) + "'><span class='stat_selector'>This Week</span></a>"
|
||||
html += " | "
|
||||
|
||||
if timekeys.get("timerange") == thismonth():
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>This Month</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"month"}) + "'><span class='stat_selector'>This Month</span></a>"
|
||||
html += " | "
|
||||
|
||||
if timekeys.get("timerange") == thisyear():
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>This Year</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,{"in":"year"}) + "'><span class='stat_selector'>This Year</span></a>"
|
||||
html += " | "
|
||||
|
||||
if timekeys.get("timerange") is None or timekeys.get("timerange").unlimited():
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>All Time</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys) + "'><span class='stat_selector'>All Time</span></a>"
|
||||
|
||||
html += "</div>"
|
||||
|
||||
if delimit:
|
||||
|
||||
#keystr = "?" + compose_querystring(keys,exclude=["step","stepn"])
|
||||
unchangedkeys = internal_to_uri({**filterkeys,**timekeys,**extrakeys})
|
||||
|
||||
# only for this element (delimit selector consists of more than one)
|
||||
unchangedkeys_sub = internal_to_uri({k:delimitkeys[k] for k in delimitkeys if k not in ["step","stepn"]})
|
||||
|
||||
html += "<div>"
|
||||
if delimitkeys.get("step") == "day" and delimitkeys.get("stepn") == 1:
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>Daily</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"day"}) + "'><span class='stat_selector'>Daily</span></a>"
|
||||
html += " | "
|
||||
|
||||
if delimitkeys.get("step") == "week" and delimitkeys.get("stepn") == 1:
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>Weekly</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"week"}) + "'><span class='stat_selector'>Weekly</span></a>"
|
||||
html += " | "
|
||||
|
||||
if delimitkeys.get("step") == "month" and delimitkeys.get("stepn") == 1:
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>Monthly</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"month"}) + "'><span class='stat_selector'>Monthly</span></a>"
|
||||
html += " | "
|
||||
|
||||
if delimitkeys.get("step") == "year" and delimitkeys.get("stepn") == 1:
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>Yearly</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"step":"year"}) + "'><span class='stat_selector'>Yearly</span></a>"
|
||||
|
||||
html += "</div>"
|
||||
|
||||
|
||||
|
||||
unchangedkeys_sub = internal_to_uri({k:delimitkeys[k] for k in delimitkeys if k != "trail"})
|
||||
|
||||
html += "<div>"
|
||||
if delimitkeys.get("trail") == 1:
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>Standard</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"trail":"1"}) + "'><span class='stat_selector'>Standard</span></a>"
|
||||
html += " | "
|
||||
|
||||
if delimitkeys.get("trail") == 2:
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>Trailing</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"trail":"2"}) + "'><span class='stat_selector'>Trailing</span></a>"
|
||||
html += " | "
|
||||
|
||||
if delimitkeys.get("trail") == 3:
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>Long Trailing</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"trail":"3"}) + "'><span class='stat_selector'>Long Trailing</span></a>"
|
||||
html += " | "
|
||||
|
||||
if delimitkeys.get("trail") == math.inf:
|
||||
html += "<span class='stat_selector' style='opacity:0.5;'>Cumulative</span>"
|
||||
else:
|
||||
html += "<a href='?" + compose_querystring(unchangedkeys,unchangedkeys_sub,{"cumulative":"yes"}) + "'><span class='stat_selector'>Cumulative</span></a>"
|
||||
|
||||
html += "</div>"
|
||||
|
||||
return html
|
|
@ -1,3 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
||||
!*.info
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env sh
|
||||
apk update
|
||||
apk add \
|
||||
gcc \
|
||||
g++ \
|
||||
python3-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
libffi-dev \
|
||||
libc-dev \
|
||||
py3-pip \
|
||||
linux-headers \
|
||||
python3 \
|
||||
py3-lxml \
|
||||
tzdata \
|
||||
vips
|
||||
|
||||
apk add py3-pip
|
||||
pip install wheel
|
||||
pip install malojaserver
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env sh
|
||||
apt update
|
||||
apt install \
|
||||
python3-pip \
|
||||
python3
|
||||
|
||||
apt install python3-pip
|
||||
pip install wheel
|
||||
pip install malojaserver
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env sh
|
||||
apk update
|
||||
apk add \
|
||||
gcc \
|
||||
g++ \
|
||||
python3-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
libffi-dev \
|
||||
libc-dev \
|
||||
py3-pip \
|
||||
linux-headers \
|
||||
python3 \
|
||||
py3-lxml \
|
||||
tzdata \
|
||||
vips
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env sh
|
||||
apt update
|
||||
apt install \
|
||||
python3-pip \
|
||||
python3
|
|
@ -1,63 +0,0 @@
|
|||
import sys, os, datetime, re, cleanup
|
||||
from cleanup import *
|
||||
from utilities import *
|
||||
|
||||
|
||||
log = open(sys.argv[1])
|
||||
outputlog = open(sys.argv[2],"w")
|
||||
checksumfile = open(sys.argv[2] + ".rulestate","w") #this file stores an identifier for all rules that were in place when the corresponding file was created
|
||||
|
||||
|
||||
c = CleanerAgent()
|
||||
stamps = [99999999999999]
|
||||
|
||||
for l in log:
|
||||
l = l.replace("\n","")
|
||||
data = l.split(",")
|
||||
|
||||
artist = data[0]
|
||||
album = data[1]
|
||||
title = data[2]
|
||||
time = data[3]
|
||||
|
||||
|
||||
(artists,title) = c.fullclean(artist,title)
|
||||
|
||||
artistsstr = "␟".join(artists)
|
||||
|
||||
|
||||
timeparts = time.split(" ")
|
||||
(h,m) = timeparts[3].split(":")
|
||||
|
||||
months = {"Jan":1,"Feb":2,"Mar":3,"Apr":4,"May":5,"Jun":6,"Jul":7,"Aug":8,"Sep":9,"Oct":10,"Nov":11,"Dec":12}
|
||||
|
||||
timestamp = int(datetime.datetime(int(timeparts[2]),months[timeparts[1]],int(timeparts[0]),int(h),int(m)).timestamp())
|
||||
|
||||
|
||||
## We prevent double timestamps in the database creation, so we technically don't need them in the files
|
||||
## however since the conversion from lastfm to maloja is a one-time thing, we should take any effort to make the file as good as possible
|
||||
if (timestamp < stamps[-1]):
|
||||
pass
|
||||
elif (timestamp == stamps[-1]):
|
||||
timestamp -= 1
|
||||
else:
|
||||
while(timestamp in stamps):
|
||||
timestamp -= 1
|
||||
|
||||
if (timestamp < stamps[-1]):
|
||||
stamps.append(timestamp)
|
||||
else:
|
||||
stamps.insert(0,timestamp)
|
||||
|
||||
|
||||
entry = "\t".join([str(timestamp),artistsstr,title,album])
|
||||
entry = entry.replace("#",r"\num")
|
||||
|
||||
outputlog.write(entry)
|
||||
outputlog.write("\n")
|
||||
|
||||
checksumfile.write(c.checksums)
|
||||
|
||||
log.close()
|
||||
outputlog.close()
|
||||
checksumfile.close()
|
332
maloja
332
maloja
|
@ -1,332 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import signal
|
||||
import os
|
||||
import stat
|
||||
import pathlib
|
||||
|
||||
|
||||
|
||||
neededmodules = [
|
||||
"bottle",
|
||||
"waitress",
|
||||
"setproctitle",
|
||||
"doreah",
|
||||
"nimrodel"
|
||||
]
|
||||
|
||||
recommendedmodules = [
|
||||
"wand"
|
||||
]
|
||||
|
||||
SOURCE_URL = "https://github.com/krateng/maloja/archive/master.zip"
|
||||
|
||||
|
||||
|
||||
def blue(txt): return "\033[94m" + txt + "\033[0m"
|
||||
def green(txt): return "\033[92m" + txt + "\033[0m"
|
||||
def yellow(txt): return "\033[93m" + txt + "\033[0m"
|
||||
|
||||
## GOTODIR goes to directory that seems to have a maloja install
|
||||
## SETUP assumes correct directory. sets settings and key
|
||||
## INSTALL ignores local files, just installs prerequisites
|
||||
## START INSTALL - GOTODIR - SETUP - starts process
|
||||
## RESTART STOP - START
|
||||
## STOP Stops process
|
||||
## UPDATE GOTODIR - updates from repo
|
||||
## LOADLASTFM GOTODIR - imports csv data
|
||||
## INSTALLHERE makes this directory valid - UPDATE - INSTALL - SETUP
|
||||
|
||||
def gotodir():
|
||||
if os.path.exists("./server.py"):
|
||||
return True
|
||||
elif os.path.exists("/opt/maloja/server.py"):
|
||||
os.chdir("/opt/maloja/")
|
||||
return True
|
||||
|
||||
print("Maloja installation could not be found.")
|
||||
return False
|
||||
|
||||
def setup():
|
||||
|
||||
from doreah import settings
|
||||
|
||||
# EXTERNAL API KEYS
|
||||
apikeys = {
|
||||
"LASTFM_API_KEY":"Last.fm API Key",
|
||||
"FANARTTV_API_KEY":"Fanart.tv API Key",
|
||||
"SPOTIFY_API_ID":"Spotify Client ID",
|
||||
"SPOTIFY_API_SECRET":"Spotify Client Secret"
|
||||
}
|
||||
|
||||
print("Various external services can be used to display images. If not enough of them are set up, only local images will be used.")
|
||||
for k in apikeys:
|
||||
key = settings.get_settings(k)
|
||||
if key is None:
|
||||
print("\t" + "Currently not using a " + apikeys[k] + " for image display.")
|
||||
elif key == "ASK":
|
||||
print("\t" + "Please enter your " + apikeys[k] + ". If you do not want to use one at this moment, simply leave this empty and press Enter.")
|
||||
key = input()
|
||||
if key == "": key = None
|
||||
settings.update_settings("settings/settings.ini",{k:key},create_new=True)
|
||||
else:
|
||||
print("\t" + apikeys[k] + " found.")
|
||||
|
||||
|
||||
# OWN API KEY
|
||||
if os.path.exists("./clients/authenticated_machines.tsv"):
|
||||
pass
|
||||
else:
|
||||
print("Do you want to set up a key to enable scrobbling? Your scrobble extension needs that key so that only you can scrobble tracks to your database. [Y/n]")
|
||||
answer = input()
|
||||
if answer.lower() in ["y","yes","yea","1","positive","true",""]:
|
||||
import random
|
||||
key = ""
|
||||
for i in range(64):
|
||||
key += str(random.choice(list(range(10)) + list("abcdefghijklmnopqrstuvwxyz") + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
|
||||
print("Your API Key: " + yellow(key))
|
||||
with open("./clients/authenticated_machines.tsv","w") as keyfile:
|
||||
keyfile.write(key + "\t" + "Default Generated Key")
|
||||
elif answer.lower() in ["n","no","nay","0","negative","false"]:
|
||||
pass
|
||||
|
||||
def install():
|
||||
toinstall = []
|
||||
toinstallr = []
|
||||
for m in neededmodules:
|
||||
try:
|
||||
exec("import " + m) #I'm sorry
|
||||
except:
|
||||
toinstall.append(m)
|
||||
|
||||
for m in recommendedmodules:
|
||||
try:
|
||||
exec("import " + m)
|
||||
except:
|
||||
toinstallr.append(m)
|
||||
|
||||
if toinstall != []:
|
||||
print("The following python modules need to be installed:")
|
||||
for m in toinstall:
|
||||
print("\t" + yellow(m))
|
||||
if toinstallr != []:
|
||||
print("The following python modules are highly recommended, some features will not work without them:")
|
||||
for m in toinstallr:
|
||||
print("\t" + yellow(m))
|
||||
|
||||
if toinstall != [] or toinstallr != []:
|
||||
if os.geteuid() != 0:
|
||||
print("You can install them with",yellow("pip install -r requirements.txt"),"or Maloja can try to install them automatically. For this, you need to run this script as a root user.")
|
||||
return False
|
||||
else:
|
||||
print("You can install them with",yellow("pip install -r requirements.txt"),"or Maloja can try to install them automatically, This might or might not work / bloat your system / cause a nuclear war.")
|
||||
fail = False
|
||||
if toinstall != []:
|
||||
print("Attempt to install required modules? [Y/n]")
|
||||
answer = input()
|
||||
|
||||
if answer.lower() in ["y","yes","yea","1","positive","true",""]:
|
||||
for m in toinstall:
|
||||
try:
|
||||
print("Installing " + m + " with pip...")
|
||||
from pip._internal import main as pipmain
|
||||
#os.system("pip3 install " + m)
|
||||
pipmain(["install",m])
|
||||
print("Success!")
|
||||
except:
|
||||
print("Failure!")
|
||||
fail = True
|
||||
|
||||
elif answer.lower() in ["n","no","nay","0","negative","false"]:
|
||||
return False #if you dont want to auto install required, you probably dont want to install recommended
|
||||
else:
|
||||
print("What?")
|
||||
return False
|
||||
if toinstallr != []:
|
||||
print("Attempt to install recommended modules? [Y/n]")
|
||||
answer = input()
|
||||
|
||||
if answer.lower() in ["y","yes","yea","1","positive","true",""]:
|
||||
for m in toinstallr:
|
||||
try:
|
||||
print("Installing " + m + " with pip...")
|
||||
from pip._internal import main as pipmain
|
||||
#os.system("pip3 install " + m)
|
||||
pipmain(["install",m])
|
||||
print("Success!")
|
||||
except:
|
||||
print("Failure!")
|
||||
fail = True
|
||||
|
||||
elif answer.lower() in ["n","no","nay","0","negative","false"]:
|
||||
return False
|
||||
else:
|
||||
print("What?")
|
||||
return False
|
||||
|
||||
if fail: return False
|
||||
print("All modules successfully installed!")
|
||||
print("Run the script again (without root) to start Maloja.")
|
||||
return False
|
||||
|
||||
else:
|
||||
print("All necessary modules seem to be installed.")
|
||||
return True
|
||||
|
||||
def getInstance():
|
||||
try:
|
||||
output = subprocess.check_output(["pidof","Maloja"])
|
||||
pid = int(output)
|
||||
return pid
|
||||
except:
|
||||
return None
|
||||
|
||||
def start():
|
||||
if install():
|
||||
|
||||
if gotodir():
|
||||
setup()
|
||||
p = subprocess.Popen(["python3","server.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
print(green("Maloja started!") + " PID: " + str(p.pid))
|
||||
|
||||
from doreah import settings
|
||||
port = settings.get_settings("WEB_PORT")
|
||||
|
||||
print("Visit your server address (Port " + str(port) + ") to see your web interface. Visit /setup to get started.")
|
||||
print("If you're installing this on your local machine, these links should get you there:")
|
||||
print("\t" + blue("http://localhost:" + str(port)))
|
||||
print("\t" + blue("http://localhost:" + str(port) + "/setup"))
|
||||
return True
|
||||
#else:
|
||||
# os.chdir("/opt/maloja/")
|
||||
# p = subprocess.Popen(["python3","server.py"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||||
# print("Maloja started! PID: " + str(p.pid))
|
||||
# return True
|
||||
|
||||
print("Error while starting Maloja.")
|
||||
return False
|
||||
|
||||
def restart():
|
||||
#pid = getInstance()
|
||||
#if pid == None:
|
||||
# print("Server is not running.")
|
||||
#else:
|
||||
# stop()
|
||||
#start()
|
||||
|
||||
wasrunning = stop()
|
||||
start()
|
||||
return wasrunning
|
||||
|
||||
def stop():
|
||||
pid = getInstance()
|
||||
if pid == None:
|
||||
print("Server is not running")
|
||||
return False
|
||||
else:
|
||||
os.kill(pid,signal.SIGTERM)
|
||||
print("Maloja stopped! PID: " + str(pid))
|
||||
return True
|
||||
|
||||
def update():
|
||||
|
||||
import urllib.request
|
||||
import shutil
|
||||
#import tempfile
|
||||
import zipfile
|
||||
import distutils.dir_util
|
||||
|
||||
if not gotodir(): return False
|
||||
|
||||
if os.path.exists("./.dev"):
|
||||
print("Better not overwrite the development server!")
|
||||
return
|
||||
|
||||
print("Updating Maloja...")
|
||||
#with urllib.request.urlopen(SOURCE_URL) as response:
|
||||
# with tempfile.NamedTemporaryFile(delete=True) as tmpfile:
|
||||
# shutil.copyfileobj(response,tmpfile)
|
||||
#
|
||||
# with zipfile.ZipFile(tmpfile.name,"r") as z:
|
||||
#
|
||||
# for f in z.namelist():
|
||||
# #print("extracting " + f)
|
||||
# z.extract(f)
|
||||
|
||||
|
||||
os.system("wget " + SOURCE_URL)
|
||||
with zipfile.ZipFile("./master.zip","r") as z:
|
||||
|
||||
# if we ever have a separate directory for the code
|
||||
# (the calling update script is not the same version as the current
|
||||
# remote repository, so we better add this check just in case)
|
||||
if "source/" in z.namelist():
|
||||
for f in z.namelist():
|
||||
if f.startswith("source/"):
|
||||
z.extract(f)
|
||||
for dir,_,files in os.walk("source"):
|
||||
for f in files:
|
||||
origfile = os.path.join(dir,f)
|
||||
newfile = ps.path.join(dir[7:],f)
|
||||
os.renames(origfile,newfile) #also prunes empty directory
|
||||
else:
|
||||
for f in z.namelist():
|
||||
z.extract(f)
|
||||
|
||||
os.remove("./master.zip")
|
||||
|
||||
|
||||
distutils.dir_util.copy_tree("./maloja-master/","./",verbose=2)
|
||||
shutil.rmtree("./maloja-master")
|
||||
print("Done!")
|
||||
|
||||
os.chmod("./maloja",os.stat("./maloja").st_mode | stat.S_IXUSR)
|
||||
|
||||
print("Make sure to update required modules! (" + yellow("pip3 install -r requirements.txt --upgrade --no-cache-dir") + ")")
|
||||
|
||||
if stop(): start() #stop returns whether it was running before, in which case we restart it
|
||||
|
||||
def loadlastfm():
|
||||
|
||||
try:
|
||||
filename = sys.argv[2]
|
||||
filename = os.path.abspath(filename)
|
||||
except:
|
||||
print("Please specify a file!")
|
||||
return
|
||||
|
||||
if gotodir():
|
||||
if os.path.exists("./scrobbles/lastfmimport.tsv"):
|
||||
print("Already imported Last.FM data. Overwrite? [y/N]")
|
||||
if input().lower() in ["y","yes","yea","1","positive","true"]:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
print("Please wait...")
|
||||
os.system("python3 ./lastfmconverter.py " + filename + " ./scrobbles/lastfmimport.tsv")
|
||||
print("Successfully imported your Last.FM scrobbles!")
|
||||
|
||||
def installhere():
|
||||
if len(os.listdir()) > 1:
|
||||
print("You should install Maloja in an empty directory.")
|
||||
return False
|
||||
else:
|
||||
open("server.py","w").close()
|
||||
# if it's cheese, but it works, it ain't cheese
|
||||
update()
|
||||
install()
|
||||
setup()
|
||||
|
||||
print("Maloja installed! Start with " + yellow("./maloja start"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1] == "start": restart()
|
||||
elif sys.argv[1] == "restart": restart()
|
||||
elif sys.argv[1] == "stop": stop()
|
||||
elif sys.argv[1] == "update": update()
|
||||
elif sys.argv[1] == "import": loadlastfm()
|
||||
elif sys.argv[1] == "install": installhere()
|
||||
else: print("Valid commands: start restart stop update import install")
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue