Added disassociation functionality

This commit is contained in:
krateng 2023-10-18 19:04:19 +02:00
parent ad824626c3
commit a3831f9b7c
10 changed files with 147 additions and 18 deletions

View File

@ -774,23 +774,25 @@ def merge_artists(target_id,source_ids):
@api.post("associate_albums_to_artist")
@authenticated_function(api=True)
@catch_exceptions
def associate_albums_to_artist(target_id,source_ids):
result = database.associate_albums_to_artist(target_id,source_ids)
def associate_albums_to_artist(target_id,source_ids,remove=False):
result = database.associate_albums_to_artist(target_id,source_ids,remove=remove)
descword = "removed" if remove else "added"
if result:
return {
"status":"success",
"desc":f"{result['target']} was added as album artist of {', '.join(src['albumtitle'] for src in result['sources'])}"
"desc":f"{result['target']} was {descword} as album artist of {', '.join(src['albumtitle'] for src in result['sources'])}"
}
@api.post("associate_tracks_to_artist")
@authenticated_function(api=True)
@catch_exceptions
def associate_tracks_to_artist(target_id,source_ids):
result = database.associate_tracks_to_artist(target_id,source_ids)
def associate_tracks_to_artist(target_id,source_ids,remove=False):
result = database.associate_tracks_to_artist(target_id,source_ids,remove=remove)
descword = "removed" if remove else "added"
if result:
return {
"status":"success",
"desc":f"{result['target']} was added as artist for {', '.join(src['title'] for src in result['sources'])}"
"desc":f"{result['target']} was {descword} as artist for {', '.join(src['title'] for src in result['sources'])}"
}
@api.post("associate_tracks_to_album")
@ -801,7 +803,7 @@ def associate_tracks_to_album(target_id,source_ids):
if result:
return {
"status":"success",
"desc":f"{', '.join(src['title'] for src in result['sources'])} were added to {result['target']['albumtitle']}"
"desc":f"{', '.join(src['title'] for src in result['sources'])} were " + f"added to {result['target']['albumtitle']}" if target_id else "removed from their album"
}

View File

@ -269,11 +269,15 @@ def merge_albums(target_id,source_ids):
@waitfordb
def associate_albums_to_artist(target_id,source_ids):
def associate_albums_to_artist(target_id,source_ids,remove=False):
sources = [sqldb.get_album(id) for id in source_ids]
target = sqldb.get_artist(target_id)
log(f"Adding {sources} into {target}")
sqldb.add_artists_to_albums(artist_ids=[target_id],album_ids=source_ids)
if remove:
log(f"Removing {sources} from {target}")
sqldb.remove_artists_from_albums(artist_ids=[target_id],album_ids=source_ids)
else:
log(f"Adding {sources} into {target}")
sqldb.add_artists_to_albums(artist_ids=[target_id],album_ids=source_ids)
result = {'sources':sources,'target':target}
dbcache.invalidate_entity_cache()
dbcache.invalidate_caches()
@ -281,11 +285,15 @@ def associate_albums_to_artist(target_id,source_ids):
return result
@waitfordb
def associate_tracks_to_artist(target_id,source_ids):
def associate_tracks_to_artist(target_id,source_ids,remove=False):
sources = [sqldb.get_track(id) for id in source_ids]
target = sqldb.get_artist(target_id)
log(f"Adding {sources} into {target}")
sqldb.add_artists_to_tracks(artist_ids=[target_id],track_ids=source_ids)
if remove:
log(f"Removing {sources} from {target}")
sqldb.remove_artists_from_tracks(artist_ids=[target_id],track_ids=source_ids)
else:
log(f"Adding {sources} into {target}")
sqldb.add_artists_to_tracks(artist_ids=[target_id],track_ids=source_ids)
result = {'sources':sources,'target':target}
dbcache.invalidate_entity_cache()
dbcache.invalidate_caches()
@ -294,10 +302,14 @@ def associate_tracks_to_artist(target_id,source_ids):
@waitfordb
def associate_tracks_to_album(target_id,source_ids):
# target_id None means remove from current album!
sources = [sqldb.get_track(id) for id in source_ids]
target = sqldb.get_album(target_id)
log(f"Adding {sources} into {target}")
sqldb.add_tracks_to_albums({src:target_id for src in source_ids})
if target_id:
target = sqldb.get_album(target_id)
log(f"Adding {sources} into {target}")
sqldb.add_tracks_to_albums({src:target_id for src in source_ids})
else:
sqldb.remove_album(source_ids)
result = {'sources':sources,'target':target}
dbcache.invalidate_entity_cache()
dbcache.invalidate_caches()
@ -350,7 +362,7 @@ def get_tracks(dbconn=None,**keys):
def get_artists(dbconn=None):
return sqldb.get_artists(dbconn=dbconn)
@waitfordb
def get_albums_artist_appears_on(dbconn=None,**keys):
artist_id = sqldb.get_artist_id(keys['artist'],dbconn=dbconn)

View File

@ -397,6 +397,14 @@ def add_tracks_to_albums(track_to_album_id_dict,replace=False,dbconn=None):
for track_id in track_to_album_id_dict:
add_track_to_album(track_id,track_to_album_id_dict[track_id],dbconn=dbconn)
@connection_provider
def remove_album(*track_ids,dbconn=None):
DB['tracks'].update().where(
DB['tracks'].c.track_id.in_(track_ids)
).values(
album_id=None
)
### these will 'get' the ID of an entity, creating it if necessary
@ -640,6 +648,29 @@ def add_artists_to_tracks(track_ids,artist_ids,dbconn=None):
return True
@connection_provider
def remove_artists_from_tracks(track_ids,artist_ids,dbconn=None):
# only tracks that have at least one other artist
subquery = DB['trackartists'].select().where(
~DB['trackartists'].c.artist_id.in_(artist_ids)
).with_only_columns(
DB['trackartists'].c.track_id
).distinct().alias('sub')
op = DB['trackartists'].delete().where(
sql.and_(
DB['trackartists'].c.track_id.in_(track_ids),
DB['trackartists'].c.artist_id.in_(artist_ids),
DB['trackartists'].c.track_id.in_(subquery.select())
)
)
result = dbconn.execute(op)
clean_db(dbconn=dbconn)
return True
@connection_provider
def add_artists_to_albums(album_ids,artist_ids,dbconn=None):
@ -655,6 +686,22 @@ def add_artists_to_albums(album_ids,artist_ids,dbconn=None):
return True
@connection_provider
def remove_artists_from_albums(album_ids,artist_ids,dbconn=None):
# no check here, albums are allowed to have zero artists
op = DB['albumartists'].delete().where(
sql.and_(
DB['albumartists'].c.album_id.in_(album_ids),
DB['albumartists'].c.artist_id.in_(artist_ids)
)
)
result = dbconn.execute(op)
clean_db(dbconn=dbconn)
return True
### Merge

View File

@ -29,6 +29,8 @@
<div class="iconsubset associateicons" data-entity_type="album" data-entity_id="{{ info.id }}" data-entity_name="{{ info.album.albumtitle }}">
{% include 'icons/add_album.jinja' %}
<!-- no remove album since that is not a specified association - every track only has one album, so the removal should
be handled on the track page (or for now, not at all) -->
{% include 'icons/association_mark.jinja' %}
{% include 'icons/association_unmark.jinja' %}
{% include 'icons/association_cancel.jinja' %}

View File

@ -39,6 +39,7 @@
<div class="iconsubset associateicons" data-entity_type="artist" data-entity_id="{{ info.id }}" data-entity_name="{{ info.artist }}">
{% include 'icons/add_artist.jinja' %}
{% include 'icons/remove_artist.jinja' %}
{% include 'icons/association_cancel.jinja' %}
</div>

View File

@ -0,0 +1,5 @@
<div class='disassociateicon clickable_icon danger' onclick="disassociate(this)" title="Disassociate artists">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M20.347 3.653a3.936 3.936 0 0 0-5.567 0l-1.75 1.75a.75.75 0 0 1-1.06-1.06l1.75-1.75a5.436 5.436 0 0 1 7.688 7.687l-1.564 1.564a.75.75 0 0 1-1.06-1.06l1.563-1.564a3.936 3.936 0 0 0 0-5.567ZM9.786 12.369a.75.75 0 0 1 1.053.125c.096.122.2.24.314.353 1.348 1.348 3.386 1.587 4.89.658l-3.922-2.858a.745.745 0 0 1-.057-.037c-1.419-1.013-3.454-.787-4.784.543L3.653 14.78a3.936 3.936 0 0 0 5.567 5.567l3-3a.75.75 0 1 1 1.06 1.06l-3 3a5.436 5.436 0 1 1-7.688-7.687l3.628-3.628a5.517 5.517 0 0 1 3.014-1.547l-7.05-5.136a.75.75 0 0 1 .883-1.213l20.25 14.75a.75.75 0 0 1-.884 1.213l-5.109-3.722c-2.155 1.709-5.278 1.425-7.232-.53a5.491 5.491 0 0 1-.431-.485.75.75 0 0 1 .125-1.053Z"></path>
</svg>
</div>

View File

@ -0,0 +1,6 @@
<div title="Remove from Album" id="removealbumicon" class="clickable_icon" onclick="removeAssociate(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M2 4.75C2 3.784 2.784 3 3.75 3h4.971a1.75 1.75 0 0 1 1.447.765l1.404 2.063a.25.25 0 0 0 .207.11h8.471c.966 0 1.75.783 1.75 1.75V19.25A1.75 1.75 0 0 1 20.25 21H4.75a.75.75 0 0 1 0-1.5h15.5a.25.25 0 0 0 .25-.25V7.688a.25.25 0 0 0-.25-.25h-8.471a1.751 1.751 0 0 1-1.447-.766L8.928 4.609a.252.252 0 0 0-.207-.109H3.75a.25.25 0 0 0-.25.25v3.5a.75.75 0 0 1-1.5 0v-3.5Z"></path>
<path d="m 9.308 12.5 a 1 0.8 0 0 1 0 1.5 H 4.09 a 1 0.8 0 0 1 0 -1.5 h 5.218 Z"></path>
</svg>
</div>

View File

@ -0,0 +1,5 @@
<div title="Remove Artist" id="removeartisticon" class="clickable_icon" onclick="removeAssociate(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 4 9.5 a 5 5 0 1 1 7.916 4.062 a 7.973 7.973 0 0 1 5.018 7.166 a 0.75 0.75 0 1 1 -1.499 0.044 a 6.469 6.469 0 0 0 -12.932 0 a 0.75 0.75 0 0 1 -1.499 -0.044 a 7.972 7.972 0 0 1 5.059 -7.181 A 4.994 4.994 0 0 1 4 9.5 Z M 9 6 a 3.5 3.5 0 1 0 0 7 a 3.5 3.5 0 0 0 0 -7 Z M 20 4 h 2.25 a 0.75 0.75 0 0 1 0 1.5 H 20 h -1.5 h -2.25 a 0.75 0.75 0 0 1 0 -1.5 h 2.25 h 0.75 Z"></path>
</svg>
</div>

View File

@ -162,7 +162,9 @@ we want icons to not be displayed in list rows, but show them with reduced opaci
.mergeicons:not(.somethingmarked_for_merge) #mergeicon, /* can't merge when nothing is selected */
.mergeicons.marked_for_merge #mergeicon, /* cant merge into one of the things we have selected */
.associateicons:not(.sources_marked_for_associate) #associatealbumicon,
.associateicons:not(.sources_marked_for_associate) #associateartisticon /* nothing marked yet, can't associate with this */
.associateicons:not(.sources_marked_for_associate) #associateartisticon,
.associateicons:not(.sources_marked_for_associate) #removealbumicon,
.associateicons:not(.sources_marked_for_associate) #removeartisticon /* nothing marked yet, can't associate with this */
{
pointer-events: none;
opacity:0.5;

View File

@ -453,6 +453,53 @@ function associate(element) {
}
function removeAssociate(element) {
const parentElement = element.closest('[data-entity_id]');
var entity_type = parentElement.dataset.entity_type;
var entity_id = parentElement.dataset.entity_id;
entity_id = parseInt(entity_id);
var requests_todo = 0;
for (var target_entity_type of associate_sources[entity_type]) {
var key = "marked_for_associate_" + target_entity_type;
var current_stored = getStoredList(key);
if (current_stored.length != 0) {
requests_todo += 1;
callback_func = function(req){
if (req.status == 200) {
toggleAssociationIcons(parentElement);
notifyCallback(req);
requests_todo -= 1;
if (requests_todo == 0) {
setTimeout(window.location.reload.bind(window.location),1000);
}
}
else {
notifyCallback(req);
}
};
neo.xhttpreq(
"/apis/mlj_1/associate_" + target_entity_type + "s_to_" + entity_type,
data={
'source_ids':current_stored,
'target_id':entity_id,
'remove': true
},
method="POST",
callback=callback_func,
json=true
);
storeList(key,[]);
}
}
}
function cancelMerge(element) {
const parentElement = element.closest('[data-entity_id]');