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: Automatically copy a value from tag A to tag B every time tag A changes. (Read 3764 times) previous topic - next topic
0 Members and 1 Guest are viewing this topic.

Automatically copy a value from tag A to tag B every time tag A changes.

The title says pretty all:

How to automatically copy a value from tag A to tag B every time tag A changes
so to keep them two tags in sync from A to B?

I usually change the value of tag A with the "Foobar controller" Android app
or with the right click menu command "Playback statistics\Rating x".

I have a masstagger script that does the copy work so I could launch that.

Thanks.

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #1
Well, of course the almighty spider monkey panel can help. Here's a draft script you can try. The "artist" and "album artist" are just for the example, replace them with the source and target tags you need. I have to warn you, though, that I did not test it thoroughly. Since the UpdateFileInfoFromJSON() method triggers the on_metadb_changed() callback, the CopyValueFromTag() function checks if the target value is actually different from the source value before updating, otherwise it would enter an infinite loop. Nevertheless the on_metadb_changed() callback is triggered by any tag update, not only the ones you want to sync.


Code: [Select]
function on_metadb_changed(handle_list, fromhook){
    CopyValueFromTag('artist', 'album artist', handle_list);
}


function CopyValueFromTag(source, target, handle_list){
    let sourceTfo = fb.TitleFormat(`%${source}%`);
    let targetTfo = fb.TitleFormat(`%${target}%`);
    handle_list.Convert().filter(handle => sourceTfo.EvalWithMetadb(handle) !== targetTfo.EvalWithMetadb(handle)).forEach(handle => {
        new FbMetadbHandleList(handle).UpdateFileInfoFromJSON(JSON.stringify([{[target]: sourceTfo.EvalWithMetadb(handle)}]));
    });
}


I don't know the two components you mention, but if they don't provide a true fromhook argument to the on_metadb_changed() callback, than you might at least restrict the triggers to manual updating by adding a conditional statement like this
Code: [Select]
function on_metadb_changed(handle_list, fromhook){
   if (!fromhook) CopyValueFromTag('artist', 'album artist', handle_list);
}
I'm late

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #2
P.S.
On a second thought, if the components you use do provide a true fromhook argument, since the UpdateFileInfoFromJSON() method does not, a true fromhook condition might make loops prevention even safer.
I'm late

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #3
Ciao Davide,  ;)

thank you for your reply. I'll look into it and see if it's OK for my need.

"Foobar controller" is an Android app that lets you control a Foobar instance you have on a PC through the HTTP control plugin.
It's a sort of a smart and interactive remote control with feedback as you can see and navigate through the playlists you have on the PC.

I use it mainly when I want to rate new music I have loaded into PC's instance of FB.
I sit down on to my Lazy Boy (a rocking chair) and drive my PC FB with my phone from there rating the track I'm listening to.
This will put the rating values into the rating field of the playback statistics FB plugin and I'd like to copy those values into my
 custom %rating% field in the file tags. Now I'm doing it manually but i'd like to let FB to do it automatically.

Thank you.

If you want to have a look at the app, it's here:
http://foobar2000controller.blogspot.com/p/how-to-start.html

I know there is another one but I didn't tested it.

Ciao.

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #4
This will put the rating values into the rating field of the playback statistics FB plugin and I'd like to copy those values into my
 custom %rating% field in the file tags. Now I'm doing it manually but i'd like to let FB to do it automatically.

Ciao Fabio,
unfortunately I don't have an android device to test the foobar controller, but from what I gather, you are actually updating the tag with playback statistic in either way. That component provides a true fromhook argument, but I'm not sure the script I posted works with it because as far as I remember it does not write to actual file tags. Perhaps this doesn't really make a difference because it should take over the %rating% mapping, but that means your custom %rating% field is not accessible with the % sign syntax, you will need the $meta() function and my script did not take this possibility into account. I'll have a look into it.
I'm late

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #5
Grazie Davide,
but don't waste too much of your time because I can cope with sincying the two values manually every now and then.
My concern it is only due to the fact that, for some reason, should the PBS db get corrupted or blanked, I would loose all my ratings. Luckily, until now I have always tagged my rating into the files so the problem would only affect the newcomers.

Do you know if one of the two rating fields is correctly recognized by other music players or apps?

Thank you.


Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #6
Grazie Davide,
but don't waste too much of your time

Don't worry, it's not a waste of time because I always learn something by trying to solve a problem. Anyway, I installed the playback statistics component and made some tests. But the problem is beyond that component: it seems that it's not possible to get any value from the rating field via the fb.TitleFormat() method, neither as '$meta(rating)', nor as '%rating%' whether playback statistics is installed or not. I don't know if this is because spider monkey panel also has a rating field in its database or because I mapped my rating field wrong or just because I suck at coding  :)) Maybe some more experienced coder has a solution.
Whatever is, the script works with any field, but rating. You could still use it to sync the playback statistics rating tag with any file tag you like, as long as it is not called rating, but TBH it would require some more safety code to prevent looping and I can't write it properly if I don't figure out why the rating value is not accessible.


Do you know if one of the two rating fields is correctly recognized by other music players or apps?

The POPM frame should be, by most players, but the playback statistics tag can only be read by foobar because it's not stored in the file metadata.


I'm late

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #7
it seems that it's not possible to get any value from the rating field via the fb.TitleFormat() method, neither as '$meta(rating)', nor as '%rating%' whether playback statistics is installed or not. I don't know if this is because spider monkey panel also has a rating field in its database or because I mapped my rating field wrong or just because I suck at coding  :))

I'm so stupid: the rating field was marked as spam in the LargeFieldsConfig file!  :-[
I revised the code specifically for the rating file tag synchronization with the playback statistics component and this should work:

Code: [Select]
function on_metadb_changed(handle_list, fromhook){
    if (fromhook) SynchronizeRatingTags(handle_list);
}


function SynchronizeRatingTags(handle_list){
    if (utils.CheckComponent('foo_playcount') && !utils.ReadTextFile(`${fb.FoobarPath}\\LargeFieldsConfig.txt`).includes('fieldSpam=rating')){
        let sourceTfo = fb.TitleFormat('[%rating%]');
        let targetTfo = fb.TitleFormat('$meta(rating)');
        handle_list.Convert().filter(handle => sourceTfo.EvalWithMetadb(handle) !== targetTfo.EvalWithMetadb(handle)).forEach(handle => {
            new FbMetadbHandleList(handle).UpdateFileInfoFromJSON(JSON.stringify([{['rating']: sourceTfo.EvalWithMetadb(handle)}]));
        });
    }
}

To be honest, I get a weird popup message saying it's not possible to update a doppelganger track in a directory that I don't even have on my pc, but I'm pretty sure it's not coming from the script, probably from something messed-up in my index-data files.
I'm late

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #8
If you change some little things, it would be faster. Have in mind the script will be running always on the background

Code: [Select]
function on_metadb_changed(handle_list, fromhook){
    if (fromhook && bHasPlugin && !bFieldCheck) {SynchronizeRatingTags(handle_list);}
}

const bHasPlugin = utils.CheckComponent('foo_playcount'); // Call it once! Not on every function call. All these things should be constant during a foobar instance
const bFieldCheck = utils.ReadTextFile(`${fb.FoobarPath}\\LargeFieldsConfig.txt`).includes('fieldSpam=rating');
const sourceTfo = fb.TitleFormat('[%rating%]');
const targetTfo = fb.TitleFormat('$meta(rating)');

function SynchronizeRatingTags(handle_list){ // This part would be faster if you evalmtdb with the entire list at once, and then update all files
        handle_list.Convert().filter(handle => sourceTfo.EvalWithMetadb(handle) !== targetTfo.EvalWithMetadb(handle)).forEach(handle => {
            new FbMetadbHandleList(handle).UpdateFileInfoFromJSON(JSON.stringify([{['rating']: sourceTfo.EvalWithMetadb(handle)}]));
        });
}

Quote
To be honest, I get a weird popup message saying it's not possible to update a doppelganger track in a directory that I don't even have on my pc, but I'm pretty sure it's not coming from the script, probably from something messed-up in my index-data files
You could check the files exist before updating the tags, pretty sure some weird bugs could appear if you don't do so.

Also check if the files are in the library, I don't think you want to change tags to other files.
Code: [Select]
let relative_paths = handle_list.GetLibraryRelativePaths();
Code: [Select]
if (fb.IsMetadbInMediaLibrary(handle)) {
...
}
Note the relative paths function outputs "" for files not on library, so it can be used as a "IsMetadbInMediaLibrary" check too, while having the path at the same time (if your use-case is only one library folder).

You can also use TF to get absolute paths along the rating, and split the string to get both
Code: [Select]
const sourceTfo = fb.TitleFormat('[%rating%]###%path%');
,,,
let stringArray= tfo.EvalWithMetadbs(handle_list);
let stringArray_length= stringArray.length;
for (let i = 0; i < stringArray_length; i++) {
let [rating, path] = stringArray[i].split('###');
//do your thing with ratings and path here
}

And obviously you can merge both TF at once and compare the 2 values
Code: [Select]
const sourceAndTargetTfo = fb.TitleFormat('[%rating%])###$meta(rating)###%path%');
...
let [rating, ratingtag, path] = stringArray[i].split('###');

Also you could use TF directly to check if [%rating%] equals $meta(rating) and return true.

Code: [Select]
const Tfo =  fb.TitleFormat('$ifequal(%rating%,$meta(rating),%path%,)')

That would output an array of the files which need re-tagging, with empty values for those who don't. The you can filter it easily... and do your thing with those paths
Code: [Select]
let stringArray= tfo.EvalWithMetadbs(handle_list).filter(Boolean)

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #9
There's a reason why UpdateFileInfoFromJSON is implemented as a handle list method only. It's so you can update many handles at once in a single operation.

Looping over a handle list and then creating a NEW handle list for each item is special kinds of stupid.  :o

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #10
Ah... there is also an option in preferences. (Though it would also write the other statistics to tags, so a script might be preferable)

Preferences > Advanced > Tools > Playback Statistics > Auto sync file tags...

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #11
Good points @regor , I appreciate that.


Looping over a handle list and then creating a NEW handle list for each item is special kinds of stupid.  :o

Thanks marc, your replies are always very useful.


There's a reason why UpdateFileInfoFromJSON is implemented as a handle list method only.

There is also a reason why I create a single handle list for each item, which is that I would have to loop anyway to create the array of values for the UpdateFileInfoFromJSON method, so I thought updating each file on the way, might actually be cleaner and faster (besides in fabiospark's case it would most of the times be a one-item list).
But truly I don't know what's better, and on my side it would require some testing on long lists (I warned it was a draft and not thoroughly tested) because I'm not able to tell by just looking at the code. Perhaps this is something that to an experienced coder is obvious and doesn't require testing at all, but you're not providing any explanation, you're just mocking. Wouldn't it  be good if in this forum we could actually discuss such issues without braggarts who can't wait to insult people? and bearing in mind that it is a forum of music player users and not of software developers? I think we would have more people writing their own scripts, rather than relying on the goodwill of a handful of developers.

I'm late

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #12
I can't believe it isn't obvious. You simply cannot write to hundreds/thousands of files at once. You might get away with it on a handful of files but what you're doing is really bad.

If you ever try and tag large amount of items PROPERLY, you'll notice fb2k will spawn a progress dialog because doing it the right way, items are queued and the files are updated sequentially, one at a time. You really do not want to test your code on large amounts of items at once.

UpdateFileInfoFromJSON will spawn the same progress dialog if the handle list contains enough items to warrant it.

Now, if you have a handle list but you don't want to tag every item in it, then you must filter it first. There are 2 ways of doing this...

Code: [Select]
var items = ...
var to_tag = fb.GetQueryItems(items, "SOME QUERY GOES HERE");
if (to_tag.Count > 0) {
var arr = [];
for (var i = 0; i< to_tag.Count; i++) {
arr.push({"tag" : "tag_value"});
}
to_tag.UpdateFileInfoFromJSON(JSON.stringify(arr));
}

or if you need to check the files exist or something you can start off with an empty handle list and fill it as you go. Then perform a single operation at the end when done.

Code: [Select]
var items = ...
var arr = [];
var to_tag = fb.CreateHandleList();
for (var i = 0; i < items.Count; i++) {
if (SOME_CONDITION) {
to_tag.Add(items[i]);
arr.push({"tag" : "tag_value"});
}
}
if (to_tag.Count > 0) {
to_tag.UpdateFileInfoFromJSON(JSON.stringify(arr));
}


edit: this was untested and had lots of typos.

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #13
If you ever try and tag large amount of items PROPERLY, you'll notice fb2k will spawn a progress dialog because doing it the right way, items are queued and the files are updated sequentially, one at a time. You really do not want to test your code on large amounts of items at once.

Thanks, that's the kind of explanation I was asking for. I appreciate your effort.  :)
I'm late

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #14
Ah... there is also an option in preferences. (Though it would also write the other statistics to tags, so a script might be preferable)

Preferences > Advanced > Tools > Playback Statistics > Auto sync file tags...

Yes, I knew that but I saw that it is disrecommended and, above all, my custom rating tag
held three different values: my rating, my wife's and the indication that it would have been
especially enjoyed with headphones.

But now I think it's better if I set the tags in a way that I can use the auto sync feature.
In the end, every time I launch a script during playback, the disrecommended scenario comes up.

Thanks to everybody for your help.

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #15
Now, if you have a handle list but you don't want to tag every item in it, then you must filter it first. There are 2 ways of doing this...

Why do you say those are the only two ways? Is there something wrong with converting the handle list to an array and than using filter() as I did (and eventually map() to build the array for UpdateFileInfoFromJSON)?
For example, ditching the multiple one handle lists, I could revise my code above as follows:

Code: [Select]
function CopyValueFromTag(source, target, handle_list){
    let sourceTfo = fb.TitleFormat(`%${source}%`);
    let targetTfo = fb.TitleFormat(`%${target}%`);
    let queryItemsArray = handle_list.Convert().filter(handle => sourceTfo.EvalWithMetadb(handle) !== targetTfo.EvalWithMetadb(handle));
if (queryItemsArray.length > 0){
newValuesArray = queryItemsArray.map(handle => {return {[target]: sourceTfo.EvalWithMetadb(handle)}});
new FbMetadbHandleList(queryItemsArray).UpdateFileInfoFromJSON(JSON.stringify(newValuesArray));
}
}

Is this also bad practice?
I'm late

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #16
Your first attempts were calling UpdateFileInfoFromJSON for every entry, that was the main problem. Your last attempt is done once for the entire list (that's right).

Btw you are evaluating the TF n*3 times doing that... While this
Code: [Select]
let to_tag = fb.GetQueryItems(fb.GetLibraryItems(), "NOT %rating% EQUAL '$meta(rating)'");;
let tag_valueArray =  fb.TitleFormat('%rating%').EvalWithMetadb(to_tag);
if (to_tag.Count > 0) {
var arr = [];
for (var i = 0; i< to_tag.Count; i++) {
arr.push({"rating" : tag_valueArray[i]});
}
to_tag.UpdateFileInfoFromJSON(JSON.stringify(arr));
}
Voila. Already gives you the filtering condition in one step and evaluates 2 times for the entire list (n size) (it takes less time than 2*n individual evaluations).

Check marc's code, you perform the query/tf eval once for lists. Note most things can be done with TF or queries...

EDIT; Obviously the code at top checks the entire library (not just your subset of songs), but you can do a similar thing given a handle list

let to_tag = fb.GetQueryItems(handle_list, ...);

I have no idea if doing the query and then getting the tags is faster than using  this though (which gives you both, the filtering and the value at once). But anyway, you must either query and get tags, or get tags and filter later. It's equivalent.

Code: [Select]
const Tfo =  fb.TitleFormat('$ifequal(%rating%,$meta(rating),%rating%,)');
const tag_valueArray = tfo.EvalWithMetadbs(handle_list);
if (tag_valueArray.filter(Boolean).length > 0) {
var arr = [];
var i = 0;
var j = 0;
while ( i < handle_list.Count) { //size changes while filtering....
if (tag_valueArray[i]) {
arr.push({"rating" : tag_valueArray[j]});
  i++;
} else {handle_list.RemoveById(i)}
j++; // but  tag_valueArray is constant
}
handle_list.UpdateFileInfoFromJSON(JSON.stringify(arr));
}

If you have time, try testing both on big lists (just comment the writing tags part):
var test = new FbProfiler('#1');
function();
test.Print();

Re: Automatically copy a value from tag A to tag B every time tag A changes.

Reply #17
Is this also bad practice?

If you prefer using array filter, it's OK but you generally want to avoid evaluating strings on a per item basis. You also have 2 instances of fb.TitleFormat which are evaluated for each item which is less than optimal. I generally would avoid commenting on it though. It's not the end of the world.

There's only one really bad thing you could do which would draw a snide remark and that's creating a new instance of fb.TitleFormat inside a loop.

Code: [Select]
//bad
fb.GetLibraryItems().Convert().forEach((item) => {
    if (fb.TitleFormat("%blah%").EvalWithMetadb(item)....
});

So long as you create an instance outside the loop and reuse it, that's OK.

My order of preference would be

-use fb.GetQueryItems where possible
-use EvalWithMetadbs (note the s) to avoid iterating every item in the handle list (here the C++ code inside the component does the heavy lifting and spitting out an array of strings when it's done should be faster than multiple EvalWithMetadb calls in JS)
-use EvalWithMetadb as a last resort. try and evaluate one instance per item if you can

edit: just use whatever you're comfortable with. I think you'd have to be dealing with thousands of items before any differences were measurable.