diff --git a/admin/schema_changes/16.sql b/admin/schema_changes/16.sql index b1f4e5377..fe266c554 100644 --- a/admin/schema_changes/16.sql +++ b/admin/schema_changes/16.sql @@ -1 +1,2 @@ ALTER TYPE entity_types ADD VALUE 'artist' AFTER 'place'; +ALTER TYPE entity_types ADD VALUE 'label' AFTER 'artist'; diff --git a/admin/sql/create_types.sql b/admin/sql/create_types.sql index ea9218c2d..1d83c6939 100644 --- a/admin/sql/create_types.sql +++ b/admin/sql/create_types.sql @@ -9,5 +9,6 @@ CREATE TYPE entity_types AS ENUM ( 'release_group', 'event', 'place', - 'artist' + 'artist', + 'label' ); diff --git a/critiquebrainz/db/review.py b/critiquebrainz/db/review.py index fc92fc19d..6da2f8740 100644 --- a/critiquebrainz/db/review.py +++ b/critiquebrainz/db/review.py @@ -20,6 +20,7 @@ "place", "release_group", "artist", + "label", ] diff --git a/critiquebrainz/frontend/__init__.py b/critiquebrainz/frontend/__init__.py index 6082ba867..5dd9b2a31 100644 --- a/critiquebrainz/frontend/__init__.py +++ b/critiquebrainz/frontend/__init__.py @@ -131,6 +131,7 @@ def create_app(debug=None, config_path=None): from critiquebrainz.frontend.views.review import review_bp from critiquebrainz.frontend.views.search import search_bp from critiquebrainz.frontend.views.artist import artist_bp + from critiquebrainz.frontend.views.label import label_bp from critiquebrainz.frontend.views.release_group import release_group_bp from critiquebrainz.frontend.views.release import release_bp from critiquebrainz.frontend.views.event import event_bp @@ -152,6 +153,7 @@ def create_app(debug=None, config_path=None): app.register_blueprint(review_bp, url_prefix='/review') app.register_blueprint(search_bp, url_prefix='/search') app.register_blueprint(artist_bp, url_prefix='/artist') + app.register_blueprint(label_bp, url_prefix='/label') app.register_blueprint(release_group_bp, url_prefix='/release-group') app.register_blueprint(release_bp, url_prefix='/release') app.register_blueprint(event_bp, url_prefix='/event') diff --git a/critiquebrainz/frontend/external/musicbrainz.py b/critiquebrainz/frontend/external/musicbrainz.py index f4711f69b..94cb113ba 100644 --- a/critiquebrainz/frontend/external/musicbrainz.py +++ b/critiquebrainz/frontend/external/musicbrainz.py @@ -44,3 +44,9 @@ def search_places(query='', limit=None, offset=None): """Search for places.""" api_resp = musicbrainzngs.search_places(query=query, limit=limit, offset=offset) return api_resp.get('place-count'), api_resp.get('place-list') + + +def search_labels(query='', limit=None, offset=None): + """Search for labels.""" + api_resp = musicbrainzngs.search_labels(query=query, limit=limit, offset=offset) + return api_resp.get('label-count'), api_resp.get('label-list') diff --git a/critiquebrainz/frontend/external/musicbrainz_db/entities.py b/critiquebrainz/frontend/external/musicbrainz_db/entities.py index 7210e20ef..c7b40cce7 100644 --- a/critiquebrainz/frontend/external/musicbrainz_db/entities.py +++ b/critiquebrainz/frontend/external/musicbrainz_db/entities.py @@ -1,10 +1,12 @@ from brainzutils.musicbrainz_db.artist import fetch_multiple_artists +from brainzutils.musicbrainz_db.label import fetch_multiple_labels from brainzutils.musicbrainz_db.place import fetch_multiple_places from brainzutils.musicbrainz_db.event import fetch_multiple_events from brainzutils.musicbrainz_db.release_group import fetch_multiple_release_groups from critiquebrainz.frontend.external.musicbrainz_db.release_group import get_release_group_by_id from critiquebrainz.frontend.external.musicbrainz_db.place import get_place_by_id from critiquebrainz.frontend.external.musicbrainz_db.event import get_event_by_id +from critiquebrainz.frontend.external.musicbrainz_db.label import get_label_by_id from critiquebrainz.frontend.external.musicbrainz_db.artist import get_artist_by_id @@ -26,6 +28,7 @@ def get_multiple_entities(entities): entities_info = {} release_group_mbids = [entity[0] for entity in filter(lambda entity: entity[1] == 'release_group', entities)] artist_mbids = [entity[0] for entity in filter(lambda entity: entity[1] == 'artist', entities)] + label_mbids = [entity[0] for entity in filter(lambda entity: entity[1] == 'label', entities)] place_mbids = [entity[0] for entity in filter(lambda entity: entity[1] == 'place', entities)] event_mbids = [entity[0] for entity in filter(lambda entity: entity[1] == 'event', entities)] entities_info.update(fetch_multiple_release_groups( @@ -35,6 +38,9 @@ def get_multiple_entities(entities): entities_info.update(fetch_multiple_artists( artist_mbids, )) + entities_info.update(fetch_multiple_labels( + label_mbids, + )) entities_info.update(fetch_multiple_places( place_mbids, )) @@ -50,6 +56,8 @@ def get_entity_by_id(id, type='release_group'): entity = get_release_group_by_id(str(id)) elif type == 'artist': entity = get_artist_by_id(str(id)) + elif type == 'label': + entity = get_label_by_id(str(id)) elif type == 'place': entity = get_place_by_id(str(id)) elif type == 'event': diff --git a/critiquebrainz/frontend/external/musicbrainz_db/label.py b/critiquebrainz/frontend/external/musicbrainz_db/label.py new file mode 100644 index 000000000..0cb48995b --- /dev/null +++ b/critiquebrainz/frontend/external/musicbrainz_db/label.py @@ -0,0 +1,23 @@ +from brainzutils import cache +from brainzutils.musicbrainz_db.label import fetch_multiple_labels +from critiquebrainz.frontend.external.musicbrainz_db import DEFAULT_CACHE_EXPIRATION +from critiquebrainz.frontend.external.relationships import label as label_rel + + +def get_label_by_id(mbid): + """Get label with MusicBrainz ID. + + Args: + mbid (uuid): MBID(gid) of the label. + Returns: + Dictionary containing the label information + """ + key = cache.gen_key(mbid) + label = cache.get(key) + if not label: + label = fetch_multiple_labels( + [mbid], + includes=['artist-rels', 'url-rels'], + ).get(mbid) + cache.set(key=key, val=label, time=DEFAULT_CACHE_EXPIRATION) + return label_rel.process(label) diff --git a/critiquebrainz/frontend/external/relationships/label.py b/critiquebrainz/frontend/external/relationships/label.py new file mode 100644 index 000000000..150684f43 --- /dev/null +++ b/critiquebrainz/frontend/external/relationships/label.py @@ -0,0 +1,76 @@ +""" +Relationship processor for label entity. +""" +import urllib.parse +from flask_babel import lazy_gettext + + +def process(label): + """Handles processing supported relation lists.""" + if 'url-rels' in label and label['url-rels']: + label['external-urls'] = _url(label['url-rels']) + return label + + +def _url(url_list): + """Processor for Label-URL relationship.""" + basic_types = { + 'wikidata': {'name': lazy_gettext('Wikidata'), 'icon': 'wikidata-16.png', }, + 'discogs': {'name': lazy_gettext('Discogs'), 'icon': 'discogs-16.png', }, + 'allmusic': {'name': lazy_gettext('Allmusic'), 'icon': 'allmusic-16.png', }, + 'bandcamp': {'name': lazy_gettext('Bandcamp'), 'icon': 'bandcamp-16.png', }, + 'official homepage': {'name': lazy_gettext('Official homepage'), 'icon': 'home-16.png', }, + 'BBC Music page': {'name': lazy_gettext('BBC Music'), }, + } + external_urls = [] + for relation in url_list: + if relation['type'] in basic_types: + external_urls.append(dict(list(relation.items()) + list(basic_types[relation['type']].items()))) + else: + try: + target = urllib.parse.urlparse(relation['target']) + if relation['type'] == 'lyrics': + external_urls.append(dict( + relation.items() + { + 'name': lazy_gettext('Lyrics'), + 'disambiguation': target.netloc, + }.items())) + elif relation['type'] == 'wikipedia': + external_urls.append(dict( + relation.items() + { + 'name': lazy_gettext('Wikipedia'), + 'disambiguation': ( + target.netloc.split('.')[0] + + ':' + + urllib.parse.unquote(target.path.split('/')[2]).decode('utf8').replace("_", " ") + ), + 'icon': 'wikipedia-16.png', + }.items())) + elif relation['type'] == 'youtube': + path = target.path.split('/') + if path[1] == 'user' or path[1] == 'channel': + disambiguation = path[2] + else: + disambiguation = path[1] + external_urls.append(dict( + relation.items() + { + 'name': lazy_gettext('YouTube'), + 'disambiguation': disambiguation, + 'icon': 'youtube-16.png', + }.items())) + elif relation['type'] == 'social network': + if target.netloc == 'twitter.com': + external_urls.append(dict( + relation.items() + { + 'name': lazy_gettext('Twitter'), + 'disambiguation': target.path.split('/')[1], + 'icon': 'twitter-16.png', + }.items())) + else: + # TODO(roman): Process other types here + pass + except Exception: # FIXME(roman): Too broad exception clause. + # TODO(roman): Log error. + pass + + return sorted(external_urls, key=lambda k: k['name']) diff --git a/critiquebrainz/frontend/static/styles/main.less b/critiquebrainz/frontend/static/styles/main.less index 47e333899..ef2509dcf 100644 --- a/critiquebrainz/frontend/static/styles/main.less +++ b/critiquebrainz/frontend/static/styles/main.less @@ -13,6 +13,7 @@ @artist-color: @blue; @event-color: @green; @place-color: @yellow; +@label-color: @blue; body { padding-bottom: 25px; @@ -131,6 +132,9 @@ ul.sharing { &.place { background-color: fade(@place-color, 70%); } + &.label { + background-color: fade(@label-color, 70%); + } &.event { background-color: fade(@event-color, 70%); } @@ -493,6 +497,9 @@ a#edit-review { margin-top: 20px; } &.place { background-color: fade(@place-color, 70%); } + &.label { + background-color: fade(@label-color, 70%); + } &.event { background-color: fade(@event-color, 70%); } diff --git a/critiquebrainz/frontend/templates/entity_review.html b/critiquebrainz/frontend/templates/entity_review.html index f5f26fa06..1e40b261e 100644 --- a/critiquebrainz/frontend/templates/entity_review.html +++ b/critiquebrainz/frontend/templates/entity_review.html @@ -5,6 +5,8 @@ artist = entity['artist-credit-phrase'] | default(_('[Unknown artist]'))) }} {% elif review.entity_type == 'artist' %} {{ _('%(artist)s', artist = ''|safe + entity.name | default(_('[Unknown artist]')) + ''|safe) }} + {% elif review.entity_type == 'label' %} + {{ _('%(label)s', label = ''|safe + entity.name | default(_('[Unknown label]')) + ''|safe) }} {% elif review.entity_type == 'event' %} {{ _('%(event)s', event = ''|safe + entity.name | default(_('[Unknown event]')) + ''|safe) }} {% elif review.entity_type == 'place' %} diff --git a/critiquebrainz/frontend/templates/label/entity.html b/critiquebrainz/frontend/templates/label/entity.html new file mode 100644 index 000000000..17ff6fbfc --- /dev/null +++ b/critiquebrainz/frontend/templates/label/entity.html @@ -0,0 +1,121 @@ +{% extends 'base.html' %} +{% from 'macros.html' import entity_rate_form, show_avg_rating with context %} + +{% block title %}{{ label.name }} - CritiqueBrainz{% endblock %} + +{% block content %} +
+

+ {{ label.name }} + {% if label.disambiguation %} + ({{ label.disambiguation }}) + {% endif %} +

+ + {% if not my_review %} + + {{ _('Write a review') }} + + {% else %} + + {{ _('Edit my review') }} + + {% endif %} +
+ +
+
+

{{ _('Reviews') }}

+ {% if not reviews %} +

{{ _('No reviews found') }}

+ {% else %} + + + + + + + + + + {% for review in reviews %} + + + + + + {% endfor %} + +
{{ _('Published on') }}{{ _('Votes (+/-)') }}
+ + {{ _('by %(reviewer)s', reviewer=' '|safe % review.user.avatar + review.user.display_name) }} + + {{ review.published_on | date }}{{ review.votes_positive_count }}/{{ review.votes_negative_count }}
+
+ +
+ {% endif %} + +
+ +
+

{{ _('Label information') }}

+ {% if label.type %}

{{ label.type }}

{% endif %} + + {% if label['external-urls'] %} + {{ _('External links') }} + + {% endif %} + + + +

+ {{ entity_rate_form('label', 'label') }} + {% if avg_rating %} +
+ {{ show_avg_rating(avg_rating.rating, avg_rating.count, show_glyphicon = false) }} +
+ {% endif %} +
+
+{% endblock %} + +{% block scripts %} + + + +{% endblock %} diff --git a/critiquebrainz/frontend/templates/macros.html b/critiquebrainz/frontend/templates/macros.html index 1acee7d0b..0c57b7cf1 100644 --- a/critiquebrainz/frontend/templates/macros.html +++ b/critiquebrainz/frontend/templates/macros.html @@ -53,6 +53,13 @@ {{ _('Artist') }} {% endif %} + {% elif entity_type == 'label' %} + + {% if overlay_type %} + + {{ _('Label') }} + + {% endif %} {% else %} {# release-group #} {% if overlay_type %} @@ -153,8 +160,10 @@ {% endif %} {% endmacro %} -{% macro show_avg_rating(rating, count) %} - +{% macro show_avg_rating(rating, count, show_glyphicon=True) %} + {% if show_glyphicon %} + + {% endif %} {{ rating }}/5 {{ _('based on %(number)s ratings', number=count) }} {% endmacro %} diff --git a/critiquebrainz/frontend/templates/navbar.html b/critiquebrainz/frontend/templates/navbar.html index 980b69eda..5677d2860 100644 --- a/critiquebrainz/frontend/templates/navbar.html +++ b/critiquebrainz/frontend/templates/navbar.html @@ -95,9 +95,10 @@
diff --git a/critiquebrainz/frontend/templates/review/browse.html b/critiquebrainz/frontend/templates/review/browse.html index b50374e99..22e47ad64 100644 --- a/critiquebrainz/frontend/templates/review/browse.html +++ b/critiquebrainz/frontend/templates/review/browse.html @@ -13,6 +13,8 @@

{{ _('Reviews') }}

{{ _('Release group') }}
  • {{ _('Artist') }}
  • +
  • + {{ _('Label') }}
  • {{ _('Event') }}
  • diff --git a/critiquebrainz/frontend/templates/review/entity/label.html b/critiquebrainz/frontend/templates/review/entity/label.html new file mode 100644 index 000000000..204a0574f --- /dev/null +++ b/critiquebrainz/frontend/templates/review/entity/label.html @@ -0,0 +1,24 @@ +{% extends 'review/entity/base.html' %} + +{% set label = review.entity_id | entity_details(type='label') %} + +{% block title %} + {% set label_title = label.name | default(_('[Unknown label]')) %} + {{ _('Review of "%(label)s" by %(user)s', label=label_title, user=review.user.display_name) }} - CritiqueBrainz +{% endblock %} + +{% block entity_title %} +

    + {% if label %} + {% set label_name = '' | safe % url_for('label.entity', id=review.entity_id) ~ label.name ~ ''|safe %} + {% else %} + {% set label_name = _('[Unknown label]') %} + {% endif %} + + {{ _('%(label)s', label=label_name) }} + + {% if label['life-span'] %} + {{ label['life-span']['begin'][:4] }} + {% endif %} +

    +{% endblock %} diff --git a/critiquebrainz/frontend/templates/review/modify/label.html b/critiquebrainz/frontend/templates/review/modify/label.html new file mode 100644 index 000000000..373000939 --- /dev/null +++ b/critiquebrainz/frontend/templates/review/modify/label.html @@ -0,0 +1,21 @@ +{% if entity is not defined %} + {% set entity = review.entity_id | entity_details(type=entity_type) %} +{% endif %} +
    +
    +
    {{ _('Label') }}
    +
    + {{ entity['name'] | default(_('[Unknown label]')) }} + {% if entity['life-span'] %} + ({{ entity['life-span']['begin'] }} - {{ entity['life-span']['end'] }}) + {% endif %} +
    +
    {{ _('Type') }}
    +
    {{ entity['type'] or '-' }}
    +
    {{ _('Country') }}
    +
    {{ entity['area'] or '-' }}
    + {% block more_info %} + {# Information like creation date, votes etc. #} + {% endblock %} +
    +
    diff --git a/critiquebrainz/frontend/templates/search/index.html b/critiquebrainz/frontend/templates/search/index.html index e4ba87a0f..80ae4d34a 100644 --- a/critiquebrainz/frontend/templates/search/index.html +++ b/critiquebrainz/frontend/templates/search/index.html @@ -26,6 +26,7 @@

    {{ _('Search') }}

    + @@ -70,6 +71,12 @@

    {{ _('Search') }}

    {{ _('Artist') }} {{ _('Type') }} + {% elif type=="label" %} + + {{ _('Name') }} + {{ _('Type') }} + {{ _('Country') }} + {% endif %} {% include 'search/results.html' %} diff --git a/critiquebrainz/frontend/templates/search/results.html b/critiquebrainz/frontend/templates/search/results.html index cde562495..67b8dcb13 100644 --- a/critiquebrainz/frontend/templates/search/results.html +++ b/critiquebrainz/frontend/templates/search/results.html @@ -64,5 +64,18 @@ + {% elif type=="label" %} + + + {{ result['name'] }} + + + {{ result['type'] or '-' }} + + + {{ result['country'] or '-' }} + + + {% endif %} {% endfor %} diff --git a/critiquebrainz/frontend/templates/search/selector.html b/critiquebrainz/frontend/templates/search/selector.html index 30a5516e0..ae3c37993 100644 --- a/critiquebrainz/frontend/templates/search/selector.html +++ b/critiquebrainz/frontend/templates/search/selector.html @@ -18,6 +18,9 @@
  • Place
  • +
  • + {{ _('Label') }} +
  • @@ -101,6 +104,26 @@

    {{ _('Place selection') }}

    +
    +

    {{ _('Label selection') }}

    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + + +
    +
    + @@ -109,6 +132,7 @@

    {{ _('Place selection') }}

    or request.args.get('release_group', default=False) or request.args.get('event', default=False) or request.args.get('place', default=False) + or request.args.get('label', default=False) %}
    {% if not results %} @@ -146,11 +170,17 @@

    {{ _('Place selection') }}

    {{ _('Location') }} + {% elif type=="label" %} + + {{ _('Name') }} + {{ _('Type') }} + {{ _('Country') }} + + {% endif %} {% include 'search/selector_results.html' %} -
    {% if count > limit %}
    diff --git a/critiquebrainz/frontend/templates/search/selector_results.html b/critiquebrainz/frontend/templates/search/selector_results.html index e90be8090..aea3696c2 100644 --- a/critiquebrainz/frontend/templates/search/selector_results.html +++ b/critiquebrainz/frontend/templates/search/selector_results.html @@ -91,5 +91,21 @@ +{% elif type=="label" %} + + + {{ result['name'] }} + + + {{ result['type'] or '-' }} + + + {{ result['country'] or '-' }} + + + + + + {% endif %} {% endfor %} diff --git a/critiquebrainz/frontend/views/__init__.py b/critiquebrainz/frontend/views/__init__.py index 1f05d8b3c..51f6c13c4 100644 --- a/critiquebrainz/frontend/views/__init__.py +++ b/critiquebrainz/frontend/views/__init__.py @@ -2,6 +2,7 @@ import critiquebrainz.db.exceptions as db_exceptions ARTIST_REVIEWS_LIMIT = 5 +LABEL_REVIEWS_LIMIT = 5 BROWSE_RELEASE_GROUPS_LIMIT = 20 diff --git a/critiquebrainz/frontend/views/label.py b/critiquebrainz/frontend/views/label.py new file mode 100644 index 000000000..86a46e43e --- /dev/null +++ b/critiquebrainz/frontend/views/label.py @@ -0,0 +1,54 @@ +from flask import Blueprint, render_template, request +from werkzeug.exceptions import NotFound +from flask_login import current_user +from flask_babel import gettext +import critiquebrainz.db.review as db_review +import critiquebrainz.frontend.external.musicbrainz_db.label as mb_label +import critiquebrainz.frontend.external.musicbrainz_db.exceptions as mb_exceptions +from critiquebrainz.frontend.views import get_avg_rating, LABEL_REVIEWS_LIMIT +from critiquebrainz.frontend.forms.rate import RatingEditForm + + +label_bp = Blueprint('label', __name__) + + +@label_bp.route('/') +def entity(id): + id = str(id) + try: + label = mb_label.get_label_by_id(id) + except mb_exceptions.NoDataFoundException: + raise NotFound(gettext("Sorry, we couldn't find a label with that MusicBrainz ID.")) + + label_reviews_limit = LABEL_REVIEWS_LIMIT + if request.args.get('reviews') == "all": + label_reviews_limit = None + + if current_user.is_authenticated: + my_reviews, my_count = db_review.list_reviews( + entity_id=label['id'], + entity_type='label', + user_id=current_user.id, + ) + my_review = my_reviews[0] if my_count else None + else: + my_review = None + + reviews_offset = 0 + reviews, reviews_count = db_review.list_reviews( + entity_id=label['id'], + entity_type='label', + sort='popularity', + limit=label_reviews_limit, + offset=reviews_offset, + ) + + avg_rating = get_avg_rating(label['id'], "label") + + rating_form = RatingEditForm(entity_id=id, entity_type='label') + rating_form.rating.data = my_review['rating'] if my_review else None + + return render_template('label/entity.html', id=label['id'], label=label, + reviews=reviews, my_review=my_review, reviews_limit=label_reviews_limit, + reviews_count=reviews_count, avg_rating=avg_rating, rating_form=rating_form, + current_user=current_user) diff --git a/critiquebrainz/frontend/views/search.py b/critiquebrainz/frontend/views/search.py index 05dc6d176..a609f8ad4 100644 --- a/critiquebrainz/frontend/views/search.py +++ b/critiquebrainz/frontend/views/search.py @@ -16,6 +16,8 @@ def search_wrapper(query, type, offset=None): count, results = musicbrainz.search_places(query, limit=RESULTS_LIMIT, offset=offset) elif type == "release-group": count, results = musicbrainz.search_release_groups(query, limit=RESULTS_LIMIT, offset=offset) + elif type == "label": + count, results = musicbrainz.search_labels(query, limit=RESULTS_LIMIT, offset=offset) else: count, results = 0, [] else: @@ -46,6 +48,7 @@ def more(): def selector(): release_group = request.args.get('release_group') artist = request.args.get('artist') + label = request.args.get('label') event = request.args.get('event') place = request.args.get('place') type = request.args.get('type') @@ -56,6 +59,8 @@ def selector(): count, results = musicbrainz.search_release_groups(release_group, limit=RESULTS_LIMIT) elif artist: count, results = musicbrainz.search_artists(artist, limit=RESULTS_LIMIT) + elif label: + count, results = musicbrainz.search_labels(label, limit=RESULTS_LIMIT) elif event: count, results = musicbrainz.search_events(event, limit=RESULTS_LIMIT) elif place: @@ -65,13 +70,14 @@ def selector(): return render_template('search/selector.html', next=next, type=type, results=results, count=count, limit=RESULTS_LIMIT, artist=artist, release_group=release_group, event=event, - place=place) + label=label, place=place) @search_bp.route('/selector/more') def selector_more(): artist = request.args.get('artist') release_group = request.args.get('release_group') + label = request.args.get('label') event = request.args.get('event') place = request.args.get('place') type = request.args.get('type') @@ -82,6 +88,8 @@ def selector_more(): limit=RESULTS_LIMIT, offset=offset) elif type == 'artist': count, results = musicbrainz.search_artists(artist, limit=RESULTS_LIMIT, offset=offset) + elif type == 'label': + count, results = musicbrainz.search_labels(label, limit=RESULTS_LIMIT, offset=offset) elif type == 'event': count, results = musicbrainz.search_events(event, limit=RESULTS_LIMIT, offset=offset) elif type == 'place':