Skip to main content

Notice

Please note that most of the software linked on this forum is likely to be safe to use. If you are unsure, feel free to ask in the relevant topics, or send a private message to an administrator or moderator. To help curb the problems of false positives, or in the event that you do find actual malware, you can contribute through the article linked here.
Topic: [SMP] Music Graph development (Read 8446 times) previous topic - next topic
0 Members and 1 Guest are viewing this topic.

Re: [SMP] Music Graph development

Reply #25
Continuing with the Graph Scripts, I'm now working on MusicIP functionalities:
-Recipes should be fully covered with current weigh/tag/queries options, since you can check any tag, set of tags, TF expressions, etc. And also pre/post filter the pool. (I would say the script gives even more possibilities)
https://www.spicefly.com/article.php?page=musicip-recipes

-Moods are trivial to replicate using a set of songs as reference. The missing 'part' would be storing those reference internally (json) instead of having the tracks on a playlist. Also I'm using only 1 track as reference, but it should be easy to 'merge' the output from a set of tracks, and only send to final playlist those which are more similar to all selected references.
https://www.spicefly.com/article.php?page=musicip-moods

-Right now I want to integrate some of the advanced playlist creation features. The current graph scoring method is more advanced than MusicIp similarity method, but it could improve on other aspects (UI, pre-defined options, etc.).
  • Scattering vocal & instrumental tracks, breaking clusters of instrumental tracks.
  • Progressive lists and intelligent list ordering, saving moods and genre/styles not in common. Can be replicated with probability of picking, since pool is ordered by scoring... so every time a track is skip , the next one has more differences compared to the reference.
  • Progressive playlist creation feature which used one track as reference, and then used the output tracks as new references, and so on.

I would appreciate some ideas or comments on the last points, now that I consider adding them.

Re: [SMP] Music Graph development

Reply #26
Last changes implemented: added the 3 features I mentioned about MusicIp.

   - Scattering vocal & instrumental tracks, breaking clusters of instrumental tracks.
   - Progressive list ordering, having more different tracks the more you advance within a playlist (using probPick < 100 && bProgressiveListOrder). Can be combined with any weight/filter preference, to suit anyone's need.
   - Progressive playlist creation, uses one track as reference, and then uses the output tracks as new references, and so on... takes more time than standard search, since it performs recursive calls...

The last feature is working pretty great, using an acoustic stoner rock song, creates a playlist which starts with stoner/hard rock tracks and then progressively changes to folk-rock tracks and psychedelic. And since it can be merged with any of the other options, you can pretty much create smart playlists in any imaginable way. With blues, it goes from blues to jazz, jazz vocal, blues, etc. Only downside is calc time, but it can be made shorter using WEIGHT method instead of GRAPH, which takes less time. Also using queries to prefilter library.

Re: [SMP] Music Graph development

Reply #27
Pardon me for asking but do you have plans for a public beta?
Quis custodiet ipsos custodes?  ;~)

Re: [SMP] Music Graph development

Reply #28
Of course, as as soon as I see it's stable enough :) (which means, not susceptible to fundamental changes) I'm changing files every day, so it makes no sense to me publishing them right now publicly... that's all (since I may break how things worked 3 days ago.) I have time to code, but not for support and/or explaining every change right now. It's not like I can upload them to github and done, since I would have to change the documentation every week.

But drop me a line and I will give you (or anyone) the current version.
regorxxx@protonmail.com

Re: [SMP] Music Graph development

Reply #29
Someone asked me about the key notation I used for similarity matching and at some point Camelot Wheel was mentioned, I have been working on it and have translated that logic (the rule of fifths) into scripts.
https://pyramind.com/harmonic-mixing-using-camelot-system-mixed-in-key/

DJs use it to create mixes/playlists with tracks which are considered 'harmonically compatible'. Usually traveling the wheel following horizontal or vertical lines in +1 steps. (there are other complex movements too). Have added that to the GRAPH script, now when using a track as reference, the key is converted to camelot system notation and it checks for all tracks with keys within that cross in a range (set at properties). Therefore creating playlists the same a DJ would do.
Obviously, that is just the key weighting... so it can be used standalone or along genre, style, dyngenre weightings,  graph method, etc.

      - Keys are supported using standard notation (Ab, Am, A#, etc.) and flat or sharp equivalences (both are translated to the same values). This standard is according to the tags acousticBrainz, piccard gives you... and the one used universally  inmusic.
      - Key matching is done using camelot wheel logic, allowing similar keys by a range using a 'cross' (changing hour or letter, but both is penalized).
      - For other key notations simple string matching will be used. That means I don't support abstract notations, like Camelot or open keys for the 'wheel logic'.(*)

(*) The reason is simple, I have no way to know if 1A, 3B, etc. is following Camelot or Open Keys notation, and since there are already scripts to convert tags to one or other system... that's user responsibility and I just use universal notation. Anyway dynamic tags can be used to create a converted key tag and I may remap the tag to add TF support too.

TODO:
      - Add key logic to 'search_similar_by', to create compatible queries instead of similarity checks. (a simplified version of the graph scripts with just queries)
      - Use current progressive list logics to create intelligent playlists with incremental keys, complex movements, etc.

Below is an example of how to set the properties to check for all tracks on library with similar genre/styles, using simple weight method and only allow those with similar Keys (on cross with 1-range).
X

X

As shown on the properties panel, the keys of the tracks in the output are: Am, C, Em. Right considering I used a track with C key as reference.



Quote
-Moods are trivial to replicate using a set of songs as reference. The missing 'part' would be storing those reference internally (json) instead of having the tracks on a playlist. Also I'm using only 1 track as reference, but it should be easy to 'merge' the output from a set of tracks, and only send to final playlist those which are more similar to all selected references.
https://www.spicefly.com/article.php?page=musicip-moods
And this is postponed until foobar native playlist format becomes public or SMP (@TheQwertiest ?) improves foobar playlist saving/loading via native methods... since I can not save a group of tracks as a mood and then load it without user intervention or loading the playlist on UI to get the handle. And it would finally make the playlist manager fully functional, supporting the native foobar playlist.

PD: I don't answer PMs here, I have given an email for a reason ;)

Re: [SMP] Music Graph development

Reply #30
New update:
-  New Option: DJ-like playlist creation with key changes following harmonic mixing rules. The entire pool (according to configurable weights, etc.) is considered, instead of using the standard playlist selection (randomly, by similarity score, etc.).

An explanation of how harmonic mixing works can be found here:
https://www.bestdjgear.net/harmonic-mixing-rules-everything-you-need-to-know/

An example would be having a track with key 7B as reference then the next 5 tracks on the playlist would have keys:
8B, 8A, 9A, 9B, 8B
X
And that would continue until it reaches the desired playlist length.

Harmonic mixing is usually done following 9 possible 'key movements', so I have added methods to describe things like  'Energy Boost' which is equivalent to moving up 1 hour in the Camelot Wheel, etc. Everytime you want a new playlist, a new pattern is created. Instead of presetting a series of movements, they are chosen randomly according to a given proportion. The result is a new playlist not only with different tracks but with its own structure everytime (so you don't expect a mood change, or a boost always at X track).

For people who like electronic music, this could replace having someone choosing the next tracks, as long as you choose an electronic track as reference (or using 'forced query ' option to only consider that genre). Anyway,it works for any genre, creating natural progressions between tracks.


So let's resume the current implementation of the Weight/graph script:
- Creates a playlist with similar tracks to the currently selected one according to genre, style, key, etc or following special rules.
- The tags to check are associated to a weight, currently they are: genre,  style, dynGenre, mood,  key,  $year(%date%), bpm, composer, customStringTag, customNumTag
- Tags can be remapped: genre ->allmusic_genre, my_genre_tag
- There are 3 methods to calculate similarity: WEIGHT, GRAPH and DYNGENRE.
- WEIGHT and DYNGENRE assign scores based on tag comparison, checking numbers within a range or complex logic comparison (i.e. keys using camelot wheel).
- GRAPH assigns genres/styles a place in a graph based on their relations, and calculates similarity by the shortest path which links them. Classifying all known musical genres is required, pretty computational expensive and is an ongoing work. (there are multiple images on the thread)
- Tracks are added to a pool if they pass the score/distance check. (i.e. score >= 70%)
- There are complex settings to filter, force queries, remove duplicates or allowing only X tracks per tag on the pool.
- Finally, tracks are selected from the pool to create a playlist. This can be done randomly, following strict scoring order, or starting with highest scored tracks but with a probability of being chosen.
- Apart from that, there are 2 more special playlist creation rules: Harmonic Mixing (previous explanation) and Progressive List Creation (recursive calls to playlist creation using new similar tracks as references) .

Re: [SMP] Music Graph development

Reply #31
Hi! regor you sent me some scripts in February, in 15 February to be more precisely. Yes, I like Top 25 Tracks , it's fast (actually I modified the script a bit, now it's Top 100). In the Playlist Manager - Add new empty playlist file , I have this error :
Playlist generation failed while writing file 'D:\foobar2000\playlist_manager\playlist.m3u8'
Except this error the Playlist Manager works fine, but as I said , I can't create new playlists. I don't know what might be, I have the last version of foobar portable and SMP.
I assume the scripts are now a bit different as you worked on them , so you can send me the scripts if they're different.

At last but not the least, we talked about a script for Most played song in a year (2017, 2015, or any other year) https://hydrogenaud.io/index.php?topic=115227.msg994727#msg994727
Can you do that script?

Re: [SMP] Music Graph development

Reply #32
Hi! regor you sent me some scripts in February, in 15 February to be more precisely. Yes, I like Top 25 Tracks , it's fast (actually I modified the script a bit, now it's Top 100).
You don't need to modify the script for that, at least if you use it within a button, Just change the associated property at the panel:
X
X
And if you are using the script function via menu or other scripts, then just change the argument.
Code: [Select]
do_top_tracks(100) 

No need touch the files at all, unless you have done other changes. Sorting and duplicates deletion are arguments too. Defaults:
Code: [Select]
do_top_tracks( number = 25, 	sortBy = "$sub(99999,%play_count%)",  checkDuplicatesBy = ["title", "artist", "date"]) 
Quote
In the Playlist Manager - Add new empty playlist file , I have this error :
Playlist generation failed while writing file 'D:\foobar2000\playlist_manager\playlist.m3u8'
Except this error the Playlist Manager works fine, but as I said , I can't create new playlists. I don't know what might be, I have the last version of foobar portable and SMP.
Not enough details... for sure I can create a new playlist without problems.
Does 'D:\foobar2000\playlist_manager\' exist?

That error only fires if the script fails to create that file 'playlist.m3u8'. Even before saving data to the file. Check the folder since that seems to be the problem. I have not added any logic to create the folder, the user must check the desired folder exists ;) (*)
 (my reasoning was the User should always configure the panel to point to their own playlist folder path, so pointing to a non existent path -like the default one- made no sense. If you wanted to use the default one, then it had to be created too.)
 
Quote
At last but not the least, we talked about a script for Most played song in a year (2017, 2015, or any other year) https://hydrogenaud.io/index.php?topic=115227.msg994727#msg994727
Can you do that script?
Will work on it this week.

(*) Anyway just added that check, so the first time a playlist is created if the folder does not exist, it's created too which is more intuitive. Thanks for the report!

Re: [SMP] Music Graph development

Reply #33
 Yes, I changed the associated property at the panel (Top 25 Tracks) , but I said I modified the script, sorry. (You are more technically, for me it's almost the same thing, haha :) Sorry once again, it's my mistake.)

With the Playlist Manager, you were right, the folder playlist_manager wasn't there, I didn't create the folder. But I'm glad you solved this little problem.
About the script for Most played song in a year....What can I say...Yesss ! :) Thanksss
I can't wait , I think it's a interesting feature (script) that it's not present in other audio players (MusicBee, jriver or aimp)

Another little problem about the Playlist Manager :
"Check playlist loaded, there are duplicated names. You can not have duplicates if using autosave. Names:
Trance, Untitled, New music"
These duplicate playlists (Trance, Untitled, New music) are not in your Playlist Manager, they're in Foobar, Playlist Switcher.
This error appears when I drag and drop a few files in one of those duplicate playlists. I don't know if it's such a big problem, I could rename those playlists but I thought you should know about this.

Re: [SMP] Music Graph development

Reply #34
Yes, I changed the associated property at the panel (Top 25 Tracks) , but I said I modified the script, sorry. (You are more technically, for me it's almost the same thing, haha :) Sorry once again, it's my mistake.)

With the Playlist Manager, you were right, the folder playlist_manager wasn't there, I didn't create the folder. But I'm glad you solved this little problem.
About the script for Most played song in a year....What can I say...Yesss ! :) Thanksss
I can't wait , I think it's a interesting feature (script) that it's not present in other audio players (MusicBee, jriver or aimp)

Another little problem about the Playlist Manager :
"Check playlist loaded, there are duplicated names. You can not have duplicates if using autosave. Names:
Trance, Untitled, New music"
These duplicate playlists (Trance, Untitled, New music) are not in your Playlist Manager, they're in Foobar, Playlist Switcher.
This error appears when I drag and drop a few files in one of those duplicate playlists. I don't know if it's such a big problem, I could rename those playlists but I thought you should know about this.
There was a similar error some time ago, but I don't know if I fixed it after sharing the files or later.

When adding tracks to playlists, ONLY if you have autosave enabled, the manager checks every playlist loaded in foobar to find a match in the playlist manager. That's why auto-saving is incompatible with duplicates names.

To solve that problem (associating an unique ID to every playlist), you must use different names or use the UUID features of the panel (right click).

Now, if you have playlist with totally different names loaded, then there should be no problems. Please, check you don't have duplicates on the manager too (check the files on the playlist folder, and their internal names with any text editor). If everything is right, then it's probably that bug I mentioned (and I will give you the latest version to test if it's solved)

EDIT: Reading you again, I think you are talking about real duplicates. Right?  i.e. you have duplicates playlist on foobar, but you don't have them on the manager at all. Yep. I check for duplicates on both sides! I could "allow" duplicates for playlist not present on the manager, although I would add it as an option and not the default behavior when using the manager (because it would be weird to have a mix of unique and duplicates playlists for regular users) .

Re: [SMP] Music Graph development

Reply #35
Yes, I have duplicate playlists on foobar, but I don't have them on the manager at all

Re: [SMP] Music Graph development

Reply #36
Then it's not a "bug". Autosave feature forces you to not have duplicates at all. You can either turn off autosaving or rename the duplicated playlists. Anyway just added the "fix", a few lines of code, thanks! (I will decide later if it becomes the default behavior or just an option)

Re: [SMP] Music Graph development

Reply #37
Quote
About the script for Most played song in a year....What can I say...Yesss ! :) Thanksss
I can't wait , I think it's a interesting feature (script) that it's not present in other audio players (MusicBee, jriver or aimp)
Got it working easily BUT there are 2 problems

- %played_times% and %lastfm_played_times% are independent variables, should the script check both? For now, I have merged them.


- The main problem: enhanced playcount only adds statistics to those variables after being installed AND tracks being played at least once. That means the script provides strange playlists when some tracks are missing those statistics. What should happen when %played_times% is empty? Should I use the standard %play_Count% or skip the track? For now, I have simply put the tracks with %played_times%. The rest is skipped.

(*) Imagine you have played track A 100 times last year, but %played_times% is empty because you just installed the component today. Using only %played_times% you would not get track A on Top N tracks playlist... using standard queries (playCount), it would be there.

Re: [SMP] Music Graph development

Reply #38
Got it. It requires the helpers from the other scripts and the buttons framework (for the button), so you must add these new files to the others. It should overwrite 2 files top_tracks.js and buttons_search_top_tracks.js
Sorry for not giving all dependencies and a standalone release, but I alreay warned this was a WIP yet. Will do asap.

As a bonus, I added a pre-filter query to both the top tracks from X year, and the standard top Tracks (from any year) scripts.
Also updated the buttons to show the pre-filter info, which can be set at the properties panel.
The playlist  for top tracks from X year script uses the year as name too.
And the year is automatically set (on the button) using current year - 1.

X

The script follows this logic:
- pre-filter query
- %last_played% > year AND %first_played% < year + 1
- Sort by total play count
- Counts %played_times% and %lastfm_played_times% within the year and merges both.
- If those arrays are null, because the user has no enhanced component or statistics updated for the tracks then:
      - use all play counts IF both %last_played% AND %first_played% == year
      - else count 1 IF %last_played% == year
      - else count 1 IF %first_played%  == year

Found the last part the best solution for those not having enhanced statistics, since it retrieves true play count if you did not played the track later than the selected year, and at least adds a play count if first/last play equals that year.

Also tracks are first sorted by play count, so even for those not having the enhanced statistics component, the script should give the best approximation to the top tracks from that year (assume the most played tracks are also the most played tracks from any year, if they were played at least once that year).

Re: [SMP] Music Graph development

Reply #39
Well, using the package manager of SMP I have created a temp standalone release for the previous script (top X tracks from year). No need for other files. Check attachment. Requires latest version of SMP. For previous versions, you need to use the standard js files (previous post).

X

X

Re: [SMP] Music Graph development

Reply #40
- %played_times% and %lastfm_played_times% are independent variables, should the script check both? For now, I have merged them.
I'm not able to check what you've done at the moment, but the component strives to ensure that the recorded times from %played_times% and %lastfm_played_times% are the same (I subtract 60 seconds from the l.fm scrobble times before saving them).

Best thing to do would probably be to get %played_times_js% and %lastfm_played_times_js%, concat the two arrays, sort, and then remove duplicates (or even duplicates within 60000ms of each other).

Re: [SMP] Music Graph development

Reply #41
Sorry for late replay, I wasn't at my pc. I tested the first release, everything seems to be ok (but I'm not 100% sure).I definitely see my most played songs in a particular year (let's say 2019 or other year)
It's nice to have a view for every year, to see what songs you listened the most in a year, not just overall. So, congrats!! :) Thanks!  I'm sure others will find this script useful too.
The only little problem that I have is that in this latest release I can't modify the year. Yes I have Top 25 Tracks 2020, but how can I modify to have Top 25 Tracks 2018 or other year?
I managed to modify the year in the first release but in the latest release no. Sorry if it's a dumb question

Re: [SMP] Music Graph development

Reply #42
- %played_times% and %lastfm_played_times% are independent variables, should the script check both? For now, I have merged them.
I'm not able to check what you've done at the moment, but the component strives to ensure that the recorded times from %played_times% and %lastfm_played_times% are the same (I subtract 60 seconds from the l.fm scrobble times before saving them).

Best thing to do would probably be to get %played_times_js% and %lastfm_played_times_js%, concat the two arrays, sort, and then remove duplicates (or even duplicates within 60000ms of each other).
That's what I did.
My "problem" was %lastfm_played_times_js% was empty but %played_times_js% was not (i don't use last fm!). So my reasoning was... that last fm variable only had last fm's plays and the other variable had foobar's plays. That's why I merged them as is  (without checking duplicates).
If that's not right according to your info, then there is a bug somewhere. (or things work different for people without last fm component?) For sure, plays within foobar do not update the last fm variable too:

X
I can't check if the opposite works, since I don't use last fm.

Anyway merging them is just a matter of using sets, to remove duplicates without sorting or comparing at all. Like:
merged = [...new set(arrayA.concat(arrayB))]

Quote
The only little problem that I have is that in this latest release I can't modify the year. Yes I have Top 25 Tracks 2020, but how can I modify to have Top 25 Tracks 2018 or other year?
They are pretty equivalent, but you have to edit the files within the package (main.js) on the second release. Otherwise they are 100% the same files.

That part controls the year. It's currently set to current year - 1, but you can edit it to whatever you want. Anyway it's just a proof of concept. If it works right, my idea is to simply put the year as a property easily configurable ;) (and setting the property to nothing would use current year -1)

X
X

EDIT: note top_tracks_from_date.js has the defaults arguments, but the button file override them. You can create your own buttons, with different dates, etc. check the examples at button. The "button file" is buttons_search_top_tracks_from_date.js in the regular release, and main.js in the package release.



Re: [SMP] Music Graph development

Reply #43
Yes, it works now, thanks for explaining, I was looking in top_tracks_from_date.js and not in main.js

Re: [SMP] Music Graph development

Reply #44
The current graph script allows for infinite possibilities of playlist creation: DJ like playlists, progressive playlists, similar by genres, by keys, by moods, by composer, etc.
Then we also have the query approach (i created 2 or 3 scripts for that). And now this Top X tracks from year.

Currently, all of them work with SMP custom menus or using buttons. I created the buttons framework to easily add/merge buttons into bars but my future aim is to create a menu panel like this:
X

Which would combine all playlist scripts within a single panel, not needing buttons for every one of them. At that point it should be pretty simple to merge anything I create with any theme. Since my idea is to augment foobar's playlist capabilities, not messing with the UI. Right now I'm just focusing on all the individual scripts for playlist creation, and later I will merge them.

Re: [SMP] Music Graph development

Reply #45
Following previous topic, I have created a contextual menu creator helper to easily add contextual menus to any panel.
Right now the usual method is creating this function
Code: [Select]
this.rbtn_up = (x, y) => {...}
And then adding manually menu entries with indexes. The main problem is later edits always require careful index checking, leaving holes for submenus, etc.
Also, every panel requires the same code. And multiple objects within the same panel require nested functions calls to append multiple menus.
A switch block is needed later to check the idx selected, so you must manually link the menu creation and the idx checking, losing even more time when coding it.
And lets not talk about changing the order of some entries at a menu once it has been already coded...



As a solution here is offered an alternative, where you simply add entries to a list and the menu is created automatically with their indexes calculated and linked to the right entries.

Code: [Select]
'use strict';

/*
Contextual Menu helper v 1.0 18/03/21
Helper to create contextual menus on demand on panels without needing to create specific methods for
every script, calculate IDs, etc. Menus are pushed to a list and created automatically, linking the entries
to their idx without needing a 'switch' block or leaving holes to ensure idx get enough numbers to expand the script.
The main utility of this helper is greatly reducing coding for simple menus and having both, the menu logic creation
and the menus' functions on the same place. Creation order is done following entry/menus addition.

Methods:
_menu({bSupressDefaultMenu = true, idxInitial = 0})
-bSupressDefaultMenu: Suppress the default context menu. left shift + left windows key will bypass it.
-idxInitial: Specifies an initial idx to create menus (useful to concatenate multiple menus objects)

.btn_up(x, y, object)
-NOTE: Called within callbacks to create the menu. Specifying an object (like another menu instance), lets you
concatenate multiple menus. Uses object.btn_up() and object.btn_up_done()
.getMainMenuName()
-NOTE: Used to get the key of the main menu. Useful to concatenate multiple menus.

.newMenu(menuName)
-menuName: Specifies the menu name or submenus names.
-NOTE: Menu is called 'main' when it's called without an argument.
-NOTE: Every menu created after the first one (main menu) will be appended to the main menu.

.newEntry({entryText = null, func = null, menuName = menuArr[0], flags = MF_STRING})
-entryText: new menu entry text. Using 'sep' or 'separator' adds a dummy separator.
-func: function associated to that entry
-menuName: to which menu/submenu the entry is associated. Uses main menu when not specified
-flags: flags for the text
-NOTE: All arguments (but 'func') may be a variable or a function (evaluated when creating the menu)

.newCheckMenu(menuName, entryTextA, entryTextB, idxFunc)
-menuName: to which menu/submenu the check is associated
-entryTextA:From entry A (idx gets calculated automatically)
-entryTextB:To entry B (idx gets calculated automatically)
-idxFunc: Logic to calculate the offset. i.e. EntryA and EntryB differ by 5 options, idxFunc must return values between 0 and 5.
-NOTE: All arguments (but 'idxFunc') may be a variable or a function (evaluated when creating the menu)

For example:
-Standard menu:
var menu = new _menu();
menu.newEntry({entryText: 'Hola', func: () => {console.log('hola')}});
menu.newEntry({entryText: 'sep'});
menu.newEntry({entryText: 'Hola2', func: () => {console.log('hola2')}});
var bSubMenu = true;
const funct = () => {return (bSubMenu) ? 'SubMenu 1' : 'SubMenu 2';};
menu.newMenu(funct);
menu.newEntry({menuName: funct, entryText: 'Change SubMenu', func: () => {bSubMenu = !bSubMenu}});
menu.newEntry({menuName: funct, entryText:'Hola 3', func: () => {console.log('hola3')}, flags: () => {return (bSubMenu) ? MF_STRING : MF_GRAYED}});
menu.newCheckMenu(funct, 'Change SubMenu', 'Hola 3', () => {return (bSubMenu) ? 0 : 1;});
menu.newEntry({entryText: 'Hola 4', func: () => {console.log('hola4')}});

function on_mouse_rbtn_up(x, y) {return menu.btn_up(x, y);}

Renders to: (note 'Hola 4' Entry is drawn after the sub-menu)
+Hola
+-----
+HOla 2
+SubMenu 1 / SubMenu 2:
+Change SubMenu
+Hola 3
+Hola 4

- Manually adding some entries to the menu (uses previous code too):
var menuTwo = new _menuTwo();
function _menuTwo() {
this.idxInitial = 0;

this.btn_up = (idxInitial) => {
this.idxInitial = idxInitial;
const menuName = menu.getMainMenuName();
menu.getMenu(menuName).AppendMenuItem(MF_STRING, idxInitial + 1, 'Manual entry 1');
}

this.btn_up_done = (currIdx) => {
if (currIdx == this.idxInitial + 1) {
console.log('Manual entry 1');
return;
} else {return;}
}
}

function on_mouse_rbtn_up(x, y) {return menu.btn_up(x, y, menuTwo);}

Renders to:
+Hola
+-----
+HOla 2
+SubMenu 1 / SubMenu 2:
+Change SubMenu
+Hola 3
+Hola 4
+Manual entry 1
 */

include(fb.ComponentPath + 'docs\\Flags.js');

function _menu({bSupressDefaultMenu = true, idxInitial = 0} = {}) {
var menuArr = [];
var menuMap = new Map();
var entryArr = [];
var entryMap = new Map();
var idxMap = new Map();
var checkMenuMap = new Map();
var checkMenuArr = [];
var idx = idxInitial;

// To create new elements
this.newMenu = (menuName = 'main') => {
menuArr.push(menuName);
if (menuArr.length > 1) {entryArr.push({menuName: menuName, bIsMenu: true});}
return menuName;
}
this.newMenu(); // Default menu

this.newEntry = ({entryText = null, func = null, menuName = menuArr[0], flags = MF_STRING}) => {
entryArr.push({entryText: entryText, func: func, menuName: menuName, flags: flags, bIsMenu: false});
return entryArr[entryArr.length -1];
}

this.newCheckMenu = (menuName, entryTextA, entryTextB, idxFun) => {
checkMenuArr.push({menuName: menuName, entryTextA: entryTextA, entryTextB: entryTextB, idxFun: idxFun});
}

// Internal
this.getMenu = (menuName) => {return (!menuName) ? menuMap : menuMap.get(menuName);}
this.getIdx = (entryText) => {return (!entryText) ? entryMap : entryMap.get(entryText);}
this.getEntry = (idx) => {return (!idx) ? idxMap : idxMap.get(idx);}
this.getCheckMenu = (menuName) => {return (!menuName) ? checkMenuMap : checkMenuMap.get(menuName);}
// External
this.getNumEntries = () => {return entryArr.length;}
this.getMainMenuName = () => {return menuArr[0]}

this.createMenu = (menuName = menuArr[0]) => {
if (_isFunc(menuName)) {menuName = menuName();}
menuMap.set(menuName, window.CreatePopupMenu());
return menuMap.get(menuName);
}

this.addToMenu = ({entryText = null, func = null, menuName = menuArr[0], flags = MF_STRING}) => {
if (entryText == 'sep' || entryText == 'separator') {menuMap.get(menuName).AppendMenuSeparator();}
else {
idx++;
if (_isFunc(menuName)) {menuName = menuName();}
if (_isFunc(flags)) {flags = flags();}
if (_isFunc(entryText)) {entryText = entryText();}
menuMap.get(menuName).AppendMenuItem(flags, idx, entryText);
entryMap.set(entryText, idx);
idxMap.set(idx, func);
}
}

this.checkMenu = (menuName, entryTextA, entryTextB, idxFunc) => {
checkMenuMap.set(menuName, () => {
if (_isFunc(menuName)) {menuName = menuName();}
if (_isFunc(entryTextA)) {entryTextA = entryTextA();}
if (_isFunc(entryTextB)) {entryTextB = entryTextB();}
return menuMap.get(menuName).CheckMenuRadioItem(this.getIdx(entryTextA), this.getIdx(entryTextB), this.getIdx(entryTextA) + idxFunc());
});
}

this.btn_up = (x, y, object) => {
// Init menus
menuArr.forEach( (menuName) => {
this.createMenu(menuName);
});
// Add entries
entryArr.forEach( (entry) => {
if (!entry.bIsMenu) { // To main menu
this.addToMenu({entryText: entry.entryText, func: entry.func, menuName: entry.menuName, flags: entry.flags});
} else { // And append sub-menus
const subMenuName = _isFunc(entry.menuName) ? entry.menuName() : entry.menuName;
if (subMenuName != menuArr[0]) {
this.getMenu(subMenuName).AppendTo(this.getMenu(menuArr[0]), MF_STRING, subMenuName)
}
}
});
// Init checks
checkMenuArr.forEach( (check) => {
this.checkMenu(check.menuName, check.entryTextA, check.entryTextB, check.idxFun);
});
this.getCheckMenu().forEach( (func) => {
func();
});
// Call other object's menu creation
if (object && object.hasOwnProperty('btn_up')) {
object.btn_up(this.getNumEntries()); // The current num of entries may be used to create another menu
}
// Find currently selected item
const currIdx = this.getMenu(menuArr[0]).TrackPopupMenu(x, y);
let bDone;
this.getEntry().forEach( (func, entryIdx) => {
if (entryIdx == currIdx) {
func();
return bDone = true;
}
});
// Call other object's menu selection
if (!bDone && object && object.hasOwnProperty('btn_up_done')) {
object.btn_up_done(currIdx);
}
// Clear all
this.clear();
return bSupressDefaultMenu;
}

this.clear = () => {
menuMap.clear();
entryMap.clear();
idxMap.clear();
checkMenuMap.clear();
idx = 0;
}
}

// Helper
function _isFunc(obj) {
  return !!(obj && obj.constructor && obj.call && obj.apply);
};

I have written some examples of use, and just 11 lines of code
Code: [Select]
			var menu = new _menu();
menu.newEntry({entryText: 'Hola', func: () => {console.log('hola')}});
menu.newEntry({entryText: 'sep'});
menu.newEntry({entryText: 'Hola2', func: () => {console.log('hola2')}});
var bSubMenu = true;
const funct = () => {return (bSubMenu) ? 'SubMenu 1' : 'SubMenu 2';};
menu.newMenu(funct);
menu.newEntry({menuName: funct, entryText: 'Change SubMenu', func: () => {bSubMenu = !bSubMenu}});
menu.newEntry({menuName: funct, entryText:'Hola 3', func: () => {console.log('hola3')}, flags: () => {return (bSubMenu) ? MF_STRING : MF_GRAYED}});
menu.newCheckMenu(funct, 'Change SubMenu', 'Hola 3', () => {return (bSubMenu) ? 0 : 1;});
menu.newEntry({entryText: 'Hola 4', func: () => {console.log('hola4')}});

function on_mouse_rbtn_up(x, y) {return menu.btn_up(x, y);}
Translate into this:
X

No need to say that maintaining a few lines where you can easily see what each entry of the menu does is easier than having the code scattered at multiple places. Also changing entries order is just a matter of moving/adding/removing lines, since idx are calculated on the fly. The script requires no additional helpers and is ready 'as is' to be included on any other script. If you want a right button menu and a left button menu, then you simply create two different _menu() objects, with their entries, and use them on the different associated callbacks

Now back to the playlist creation topic, this helper can be used to create arbitrary menus linked to other scripts (like playlist creation functions). Similar to the image at bottom at the previous reply.

Code: [Select]
include(fb.ProfilePath + 'scripts\\SMP\\xxx-scripts\\top_tracks_from_date.js');
var menu = new _menu();
var year = new Date().getFullYear();
menu.newEntry({entryText: 'Top Tracks from ' + year, func: () => {do_top_tracks_from_date({playlistLength: 50,  year: year})}});
year--;
menu.newEntry({entryText: 'Top Tracks from ' + year, func: () => {do_top_tracks_from_date({playlistLength: 50,  year: year})}});
year--;
menu.newEntry({entryText: 'Top Tracks from ' + year, func: () => {do_top_tracks_from_date({playlistLength: 50,  year: year})}});

function on_mouse_rbtn_up(x, y) {return menu.btn_up(x, y);}
...
Translates into:
X

Full code is at top. Is wip, and at some point I will share it too on github along all scripts.

Re: [SMP] Music Graph development

Reply #46
And with a few more work, we can merge the other scripts too:
Code: [Select]
'use strict';
include(fb.ProfilePath + 'scripts\\SMP\\xxx-scripts\\helpers\\menu_creator.js');

var menu = new _menu();
const playlistSize = 50;

function on_mouse_rbtn_up(x, y) {return menu.btn_up(x, y);}

// Top Tracks from year
include(fb.ProfilePath + 'scripts\\SMP\\xxx-scripts\\top_tracks_from_date.js');
var menuName = menu.newMenu('Top Tracks from...');
{ // Scopes force function to use the right variable at execution
const selYear = new Date().getFullYear();
menu.newEntry({menuName: menuName, entryText: 'Top Tracks from ' + selYear, func: (year = selYear) => {do_top_tracks_from_date({playlistLength: playlistSize,  year: year})}});
}
{
const selYear = new Date().getFullYear() - 1;
menu.newEntry({menuName: menuName, entryText: 'Top Tracks from ' + selYear, func: (year = selYear) => {do_top_tracks_from_date({playlistLength: playlistSize,  year: year})}});
}
{
const selYear = new Date().getFullYear() - 2;
menu.newEntry({menuName: menuName, entryText: 'Top Tracks from ' + selYear, func: (year = selYear) => {do_top_tracks_from_date({playlistLength: playlistSize,  year: year})}});
}
menu.newEntry({menuName: menuName, entryText: 'sep'});
menu.newEntry({menuName: menuName, entryText: 'Set year... ', func: () => {
const selYear = new Date().getFullYear();
const input = Number(utils.InputBox(window.ID, 'Enter year', window.Name, selYear));
if (!Number.isSafeInteger(input)) {return;}
do_top_tracks_from_date({playlistLength: 50,  year: input})
}});
menu.newEntry({entryText: 'sep'});
// Similar by...
include(fb.ProfilePath + 'scripts\\SMP\\xxx-scripts\\search_similar_by.js');
var menuName = menu.newMenu('Search similar by...');
{ // Scopes force function to use the right variable at execution
const selSimilarBy = {mood: 6};
menu.newEntry({menuName: menuName, entryText: 'Moods', func: (similarBy = selSimilarBy) => {do_search_similar_by(playlistSize, undefined, undefined, undefined, similarBy, undefined)}});
}
{
const selSimilarBy = {genre: 2};
menu.newEntry({menuName: menuName, entryText: 'Genres', func: (similarBy = selSimilarBy) => {do_search_similar_by(playlistSize, undefined, undefined, undefined, similarBy, undefined)}});
}
{
const selSimilarBy = {style: 2};
menu.newEntry({menuName: menuName, entryText: 'Styles', func: (similarBy = selSimilarBy) => {do_search_similar_by(playlistSize, undefined, undefined, undefined, similarBy, undefined)}});
}
menu.newEntry({entryText: 'sep'});
// Same Style
include(fb.ProfilePath + 'scripts\\SMP\\xxx-scripts\\search_same_style.js');
menu.newEntry({entryText: 'Same Styles', func: () => {do_search_same_style(playlistSize)}});
// Same Style & Moods
include(fb.ProfilePath + 'scripts\\SMP\\xxx-scripts\\search_same_style_moods.js');
menu.newEntry({entryText: 'Same Styles and Moods', func: () => {do_search_same_style_moods(playlistSize)}});

X

Will continue working on it and adding all scripts.

Re: [SMP] Music Graph development

Reply #47
That's what I did.
My "problem" was %lastfm_played_times_js% was empty but %played_times_js% was not (i don't use last fm!). So my reasoning was... that last fm variable only had last fm's plays and the other variable had foobar's plays. That's why I merged them as is  (without checking duplicates).
If that's not right according to your info, then there is a bug somewhere. (or things work different for people without last fm component?) For sure, plays within foobar do not update the last fm variable too:
The issue is that %played_times_js% will also include any plays scrobbled by your same foobar instance. You'll need to attempt to remove duplicates, otherwise you'll double count plays for anyone scrobbling from foobar. You also need do things a little fuzzy because the exact seconds might not match. That's why removing anything within 30 seconds of the last play is a good idea (since you can't scrobble any track less than 30 seconds long).

Re: [SMP] Music Graph development

Reply #48
That's what I did.
My "problem" was %lastfm_played_times_js% was empty but %played_times_js% was not (i don't use last fm!). So my reasoning was... that last fm variable only had last fm's plays and the other variable had foobar's plays. That's why I merged them as is  (without checking duplicates).
If that's not right according to your info, then there is a bug somewhere. (or things work different for people without last fm component?) For sure, plays within foobar do not update the last fm variable too:
The issue is that %played_times_js% will also include any plays scrobbled by your same foobar instance. You'll need to attempt to remove duplicates, otherwise you'll double count plays for anyone scrobbling from foobar. You also need do things a little fuzzy because the exact seconds might not match. That's why removing anything within 30 seconds of the last play is a good idea (since you can't scrobble any track less than 30 seconds long).
I think I'm really not understanding their usage at all. Any of these is right? (lastfm statistics being any last fm play, within web + scrobbles)

A)  %played_times_js%  contains both, foobar (1) and lastfm statistics (2+3), and %lastfm_played_times_js% contains only lastfm statistics but with some temp offset (2+3).
B)  %played_times_js%  contains both, foobar (1)  and lastfm scrobbles (3), done within foobar and %lastfm_played_times_js% contains lastfm statistics (2+3) (with some temp offset).

In any case, what's the reasoning of having duplicate data at all? Why are they not being filtered using the SDK? Or why not exposing a global variable having all without duplicates?

Re: [SMP] Music Graph development

Reply #49
Following the dynamic menu creation, some updates. Have merged all buttons from here and converted them into menus, adding some more.



Left to do:
-'similar artist' using last.fm data. Instead of using marc's auto-playlist approach which only created playlist with 1 artist (clickable), the idea is to merge the entire list and give a playlist with tracks from all of them.
-More special playlists
  +Anti-influences
  +Harmonic mixing with other parameters
  +Progressive Playlist with influences
-More predefined playlists using graph, etc.
  +No weights, just by graph
  +Configure scattering instrumentals, etc. (with radius checks)

 
SimplePortal 1.0.0 RC1 © 2008-2021