Skip to content

Commit

Permalink
Merge branch 'master' into volume-slider
Browse files Browse the repository at this point in the history
  • Loading branch information
Suvid-Singhal authored Jan 8, 2025
2 parents 602ecaa + b030773 commit 0cc8fe3
Show file tree
Hide file tree
Showing 16 changed files with 148 additions and 153 deletions.
5 changes: 5 additions & 0 deletions frontend/css/listens-page.less
Original file line number Diff line number Diff line change
Expand Up @@ -586,3 +586,8 @@
color: unset !important;
text-decoration: unset !important;
}

.link-listens-tooltip {
border-bottom: 1px dotted #000;
text-decoration: none;
}
26 changes: 23 additions & 3 deletions frontend/js/src/common/flairs/FlairsExplanationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import { get as _get } from "lodash";
import NiceModal, { useModal } from "@ebay/nice-modal-react";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";

/** A note about this modal:
* We use Bootstrap 3 modals, which work with jQuery and data- attributes
Expand All @@ -14,6 +14,7 @@ import { Link } from "react-router-dom";

export default NiceModal.create(() => {
const modal = useModal();
const navigate = useNavigate();

const closeModal = () => {
modal.hide();
Expand Down Expand Up @@ -73,8 +74,27 @@ export default NiceModal.create(() => {
</p>

<p>
Ready to support us? <Link to="/donate/">Donate here</Link> or{" "}
<Link to="/donors/">view our donors</Link>.
Ready to support us?{" "}
<Link
to="/donate/"
onClick={() => {
navigate("/donate/");
}}
data-dismiss="modal"
>
Donate here
</Link>{" "}
or{" "}
<Link
to="/donors/"
onClick={() => {
navigate("/donors/");
}}
data-dismiss="modal"
>
view our donors
</Link>
.
</p>
</div>
<div className="modal-footer">
Expand Down
15 changes: 8 additions & 7 deletions frontend/js/src/settings/link-listens/LinkListens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,15 +249,16 @@ export default function LinkListensPage() {
first.
</ReactTooltip>
<p>
You will find below your top 1000 listens (grouped by album) that have
not been automatically linked
<FontAwesomeIcon
icon={faQuestionCircle}
size="sm"
You will find below your top 1000 listens (grouped by album) that
have&nbsp;
<u
className="link-listens-tooltip"
data-tip
data-for="matching-tooltip"
/>{" "}
to a MusicBrainz recording. Link them below or&nbsp;
>
not been automatically linked
</u>
&nbsp; to a MusicBrainz recording. Link them below or&nbsp;
<a href="https://wiki.musicbrainz.org/How_to_Contribute">
submit new data to MusicBrainz
</a>
Expand Down
8 changes: 7 additions & 1 deletion frontend/js/src/user/components/follow/UserSocialNetwork.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ export default class UserSocialNetwork extends React.Component<
user: ListenBrainzUser,
action: "follow" | "unfollow"
) => {
const { currentUser } = this.context;
const { user: profileUser } = this.props;
const { followingList } = this.state;
const newFollowingList = [...followingList];
const index = newFollowingList.findIndex(
Expand All @@ -260,7 +262,11 @@ export default class UserSocialNetwork extends React.Component<
if (action === "follow" && index === -1) {
newFollowingList.push(user.name);
}
if (action === "unfollow" && index !== -1) {
if (
action === "unfollow" &&
index !== -1 &&
profileUser.name === currentUser?.name
) {
newFollowingList.splice(index, 1);
}
this.setState({ followingList: newFollowingList });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ export default NiceModal.create((props: ImportPLaylistModalProps) => {
title="Successfully imported playlist from Apple Music"
message={
<>
Imported
<a href={newPlaylist.identifier}> {playlistName}</a>
Imported{" "}
<Link to={`/playlist/${newPlaylist.identifier}`}>
{playlistName}
</Link>
</>
}
/>,
Expand Down
52 changes: 42 additions & 10 deletions frontend/js/tests/user/follow/UserSocialNetwork.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const similarUsers = [
},
];

const followingFollowers = ["bob", "fnord"];
const followingFollowers = ["jack", "fnord"];

describe("<UserSocialNetwork />", () => {
afterEach(() => {
Expand Down Expand Up @@ -138,7 +138,7 @@ describe("<UserSocialNetwork />", () => {
];
expect(instance.state.similarUsersList).toEqual(similarUsersInState);

const expectedFollowingFollowersState = ["bob", "fnord"];
const expectedFollowingFollowersState = ["jack", "fnord"];
expect(instance.state.followerList).toEqual(
expectedFollowingFollowersState
);
Expand Down Expand Up @@ -166,14 +166,44 @@ describe("<UserSocialNetwork />", () => {
});

// initial state after first fetch
expect(instance.state.followingList).toEqual(["bob", "fnord"]);
expect(instance.state.followingList).toEqual(["jack", "fnord"]);
await act(async () => {
instance.updateFollowingList({ name: "Baldur" }, "follow");
});
expect(instance.state.followingList).toEqual(["bob", "fnord", "Baldur"]);
expect(instance.state.followingList).toEqual(["jack", "fnord", "Baldur"]);
});

it("updates the state when called with action unfollow", async () => {
const wrapper = mount(
<GlobalAppContext.Provider value={globalContext}>
<ReactQueryWrapper>
<BrowserRouter>
<UserSocialNetwork
user={{
id: 1,
name: "iliekcomputers",
}}
/>
</BrowserRouter>
</ReactQueryWrapper>
</GlobalAppContext.Provider>
);
const instance = wrapper
.find(UserSocialNetwork)
.instance() as UserSocialNetwork;
await act(async () => {
await instance.componentDidMount();
});

// initial state after first fetch
expect(instance.state.followingList).toEqual(["jack", "fnord"]);
await act(async () => {
instance.updateFollowingList({ name: "fnord" }, "unfollow");
});
expect(instance.state.followingList).toEqual(["jack"]);
});

it("does nothing, when called with action unfollow when it's not your own account", async () => {
const wrapper = mount(
<GlobalAppContext.Provider value={globalContext}>
<ReactQueryWrapper>
Expand All @@ -191,11 +221,13 @@ describe("<UserSocialNetwork />", () => {
});

// initial state after first fetch
expect(instance.state.followingList).toEqual(["bob", "fnord"]);
expect(instance.state.followingList).toEqual(["jack", "fnord"]);
await act(async () => {
instance.updateFollowingList({ name: "fnord" }, "unfollow");
});
expect(instance.state.followingList).toEqual(["bob"]);

// Since it's not your own account, it should not be removed from the following list
expect(instance.state.followingList).toEqual(["jack", "fnord"]);
});

it("only allows adding a user once", async () => {
Expand All @@ -217,13 +249,13 @@ describe("<UserSocialNetwork />", () => {
await act(async () => {
instance.updateFollowingList({ name: "Baldur" }, "follow");
});
expect(instance.state.followingList).toEqual(["bob", "fnord", "Baldur"]);
expect(instance.state.followingList).toEqual(["jack", "fnord", "Baldur"]);

// Ensure we can't add a user twice
await act(async () => {
instance.updateFollowingList({ name: "Baldur" }, "follow");
});
expect(instance.state.followingList).toEqual(["bob", "fnord", "Baldur"]);
expect(instance.state.followingList).toEqual(["jack", "fnord", "Baldur"]);
});

it("does nothing when trying to unfollow a user that is not followed", async () => {
Expand All @@ -243,11 +275,11 @@ describe("<UserSocialNetwork />", () => {
await instance.componentDidMount();
});

expect(instance.state.followingList).toEqual(["bob", "fnord"]);
expect(instance.state.followingList).toEqual(["jack", "fnord"]);
await act(async () => {
instance.updateFollowingList({ name: "Baldur" }, "unfollow");
});
expect(instance.state.followingList).toEqual(["bob", "fnord"]);
expect(instance.state.followingList).toEqual(["jack", "fnord"]);
});
});

Expand Down
14 changes: 7 additions & 7 deletions listenbrainz/db/couchdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,15 @@ def fetch_exact_data(database: str, document_id: str):

def insert_data(database: str, data: list[dict]):
""" Insert the given data into the specified database. """
with start_span(op="serializing", description="serialize data to json"):
with start_span(op="serializing", name="serialize data to json"):
docs = orjson.dumps({"docs": data})

with start_span(op="http", description="insert docs in couchdb using api"):
with start_span(op="http", name="insert docs in couchdb using api"):
couchdb_url = f"{get_base_url()}/{database}/_bulk_docs"
response = requests.post(couchdb_url, data=docs, headers={"Content-Type": "application/json"})
response.raise_for_status()

with start_span(op="deserializing", description="checking response for conflicts"):
with start_span(op="deserializing", name="checking response for conflicts"):
conflict_doc_ids = []
for doc_status in response.json():
if doc_status.get("error") == "conflict":
Expand All @@ -165,15 +165,15 @@ def insert_data(database: str, data: list[dict]):

conflict_docs = orjson.dumps({"docs": [{"id": doc_id} for doc_id in conflict_doc_ids]})

with start_span(op="http", description="retrieving conflicts from database"):
with start_span(op="http", name="retrieving conflicts from database"):
response = requests.post(
f"{get_base_url()}/{database}/_bulk_get",
data=conflict_docs,
headers={"Content-Type": "application/json"}
)
response.raise_for_status()

with start_span(op="deserializing", description="processing conflicting revisions"):
with start_span(op="deserializing", name="processing conflicting revisions"):
revs_map = {}
for result in response.json()["results"]:
existing_doc = result["docs"][0]["ok"]
Expand All @@ -185,10 +185,10 @@ def insert_data(database: str, data: list[dict]):
doc["_rev"] = revs_map[doc["_id"]]
docs_to_update.append(doc)

with start_span(op="serializing", description="serialize conflicting docs to update"):
with start_span(op="serializing", name="serialize conflicting docs to update"):
docs_to_update = orjson.dumps({"docs": docs_to_update})

with start_span(op="http", description="retry updating conflicts in database"):
with start_span(op="http", name="retry updating conflicts in database"):
response = requests.post(couchdb_url, data=docs_to_update, headers={"Content-Type": "application/json"})
response.raise_for_status()

Expand Down
2 changes: 1 addition & 1 deletion listenbrainz/db/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def insert(database: str, from_ts: int, to_ts: int, values: list[dict], key="use
values: list with each item as stat for 1 user
key: the key of the value to user as _id of the document
"""
with start_span(op="processing", description="add _id, from_ts, to_ts and last_updated to docs"):
with start_span(op="processing", name="add _id, from_ts, to_ts and last_updated to docs"):
for doc in values:
doc["_id"] = str(doc[key])
doc["key"] = doc[key]
Expand Down
4 changes: 2 additions & 2 deletions listenbrainz/tests/integration/test_settings_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ def test_delete_listens(self):
with self.app.app_context():
task = get_task()
self.assertIsNotNone(task)
self.assertEquals(task.user_id, self.user["id"])
self.assertEquals(task.task, "delete_listens")
self.assertEqual(task.user_id, self.user["id"])
self.assertEqual(task.task, "delete_listens")

# wait for background tasks to be processed -- max 30s allowed for the test to pass

Expand Down
20 changes: 0 additions & 20 deletions listenbrainz/tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

import listenbrainz.utils as utils
import pika
import time
import unittest
import uuid
Expand All @@ -32,25 +31,6 @@ class ListenBrainzUtilsTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app(debug=True) # create an app for config value access

def test_create_channel_to_consume(self):
connection = utils.connect_to_rabbitmq(
username=self.app.config['RABBITMQ_USERNAME'],
password=self.app.config['RABBITMQ_PASSWORD'],
host=self.app.config['RABBITMQ_HOST'],
port=self.app.config['RABBITMQ_PORT'],
virtual_host=self.app.config['RABBITMQ_VHOST'],
error_logger=print,
)

ch = utils.create_channel_to_consume(
connection=connection,
exchange='test',
queue='test',
callback_function=lambda a, b, c, d: None
)
self.assertIsNotNone(ch)
self.assertIsInstance(ch, pika.adapters.blocking_connection.BlockingChannel)

def test_unix_timestamp_to_datetime(self):
t = int(time.time())
x = utils.unix_timestamp_to_datetime(t)
Expand Down
43 changes: 0 additions & 43 deletions listenbrainz/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import errno
import os
import socket
import time
from datetime import datetime, timezone

import pika


def create_path(path):
"""Creates a directory structure if it doesn't exist yet."""
Expand All @@ -17,46 +14,6 @@ def create_path(path):
(path, exception))


def connect_to_rabbitmq(username, password,
host, port, virtual_host,
connection_type=pika.BlockingConnection,
credentials_type=pika.PlainCredentials,
error_logger=print,
error_retry_delay=3,
heartbeat=None,
connection_name: str = None):
"""Connects to RabbitMQ
Args:
username, password, host, port, virtual_host
error_logger: A function used to log failed connections.
connection_type: A pika Connection class to instantiate.
credentials_type: A pika Credentials class to use.
error_retry_delay: How long to wait in seconds before retrying a connection.
Returns:
A connection, with type of connection_type.
"""
while True:
try:
if connection_name is None:
connection_name = get_fallback_connection_name()
credentials = credentials_type(username, password)
connection_parameters = pika.ConnectionParameters(
host=host,
port=port,
virtual_host=virtual_host,
credentials=credentials,
heartbeat=heartbeat,
client_properties={"connection_name": connection_name}
)
return connection_type(connection_parameters)
except Exception as err:
error_message = "Cannot connect to RabbitMQ: {error}, retrying in {delay} seconds."
error_logger(error_message.format(error=str(err), delay=error_retry_delay))
time.sleep(error_retry_delay)


def init_cache(host, port, namespace):
""" Initializes brainzutils cache. """
from brainzutils import cache
Expand Down
Loading

0 comments on commit 0cc8fe3

Please sign in to comment.