Skip to content

Commit

Permalink
Merge branch 'persist-recent-broadcasts'
Browse files Browse the repository at this point in the history
  • Loading branch information
alecigne committed Dec 30, 2022
2 parents 9bc1e1b + 6908308 commit 085dd3d
Show file tree
Hide file tree
Showing 25 changed files with 1,115 additions and 106 deletions.
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM openjdk:17-alpine
COPY target/somafm-song-history-0.2.0-jar-with-dependencies.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
90 changes: 86 additions & 4 deletions README.org
Original file line number Diff line number Diff line change
@@ -1,19 +1,101 @@
#+title: =somafm-song-history=

Command line application that retrieves and prints SomaFM's song
history in the console. This is a first step towards building a real
database of broadcasts.
Application that retrieves and prints [[https://somafm.com/][SomaFM]]'s recently played songs
in the console, or save it to a database.

Please support SomaFM's awesome work [[https://somafm.com/support/][here]].

* About
:PROPERTIES:
:CREATED: [2022-12-30 Fri 12:00]
:END:

This project is developed for my personal use, but I'll be glad to
help if you encounter any [[https://github.com/alecigne/somafm-song-history/issues][issue]].

My goal is to build and browse a personal database of songs played by
SomaFM, and as an ambient fan, especially Drone Zone.

As stated on [[https://somafm.com/linktous/api.html][this page]]:

#+begin_quote
SomaFM API is no longer available to third parties
#+end_quote

Thus this application works by parsing SomaFM's "Recently Played
Songs" page (example [[https://somafm.com/dronezone/songhistory.html][here]]).

* Usage
:PROPERTIES:
:CREATED: [2022-11-02 Wed 19:00]
:END:

** v0.3 (next)
:PROPERTIES:
:CREATED: [2022-12-30 Fri 11:50]
:END:

Goals for v0.3:

- Use a scheduler to retrieve history at regular intervals. History
cannot be displayed in the console anymore.
- Expose a REST API of recently played songs.

** v0.2 (current)
:PROPERTIES:
:CREATED: [2022-12-30 Fri 11:49]
:END:

1. You will need a PostgreSQL database running. This can be achieved
using Docker and the PostgreSQL official image:

#+begin_src sh
docker run \
--name postgres-db \
-e POSTGRES_PASSWORD=mysecretpassword \
-p 5432:5432 \
-d postgres
#+end_src

The default user is =postgres=.

2. Either download the provided image (TODO) or build it:

#+begin_src sh
docker image build -t somafm-song-history .
#+end_src

3. Run the corresponding container with your credentials of choice:

#+begin_src sh
docker run \
-e DB_URL="jdbc:postgresql://localhost:5432/postgres" \
-e DB_USER="postgres" \
-e DB_PASSWORD="mysecretpassword" \
--net=host \
somafm-song-history "display" "Drone Zone"
#+end_src

Two arguments must be provided: an /action/ (=display= or =save=) and
a /channel/ (e.g. =Groove Salad=). The channel name is its public
name, available on [[https://somafm.com/#alpha][this page]]. Without a channel, the program will
default to Drone Zone.

** v0.1
:PROPERTIES:
:CREATED: [2022-12-30 Fri 11:48]
:END:

This version only displays the history in the console.

#+begin_src sh
java -jar somafm-song-history-0.1.0.jar "Beat Blender"
#+end_src

Without a channel, the program will default to Drone Zone.
(with Java 17)

The channel name is the public name, available on [[https://somafm.com/#alpha][this page]]. Without a
channel, the program will default to Drone Zone.

* Known bugs
:PROPERTIES:
Expand Down
76 changes: 74 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>net.lecigne</groupId>
<artifactId>somafm-song-history</artifactId>
<version>0.1.0</version>
<version>0.2.0</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
Expand All @@ -15,9 +15,39 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.testing.version>2.22.2</maven.testing.version>
<log4j.version>2.19.0</log4j.version>
<flyway.version>9.10.2</flyway.version>
<testcontainers.version>1.17.6</testcontainers.version>
</properties>

<dependencies>
<!-- Persistence -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.5.0</version>
</dependency>
<!-- Other -->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
Expand Down Expand Up @@ -77,7 +107,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.8.1</version>
<version>4.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand All @@ -92,6 +122,36 @@
<version>1.2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>logcaptor</artifactId>
<version>2.7.10</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand All @@ -100,10 +160,22 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.testing.version}</version>
<configuration>
<!-- For LogCaptor -->
<classpathDependencyExcludes>
<classpathDependencyExclude>org.apache.logging.log4j:log4j-slf4j-impl</classpathDependencyExclude>
</classpathDependencyExcludes>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven.testing.version}</version>
<configuration>
<!-- For LogCaptor -->
<classpathDependencyExcludes>
<classpathDependencyExclude>org.apache.logging.log4j:log4j-slf4j-impl</classpathDependencyExclude>
</classpathDependencyExcludes>
</configuration>
<executions>
<execution>
<goals>
Expand Down
27 changes: 20 additions & 7 deletions src/main/java/net/lecigne/somafm/SomaFmSongHistory.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package net.lecigne.somafm;

import static net.lecigne.somafm.config.Configuration.ROOT_CONFIG;
import static net.lecigne.somafm.model.Channel.DRONE_ZONE;
import static net.lecigne.somafm.config.SomaFmConfig.ROOT_CONFIG;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigBeanFactory;
import com.typesafe.config.ConfigFactory;
import java.time.ZoneId;
import lombok.extern.slf4j.Slf4j;
import net.lecigne.somafm.business.RecentBroadcastBusiness;
import net.lecigne.somafm.config.Configuration;
import net.lecigne.somafm.cli.CLI;
import net.lecigne.somafm.config.SomaFmConfig;
import org.flywaydb.core.Flyway;

@Slf4j
public class SomaFmSongHistory {

/**
Expand All @@ -23,11 +26,21 @@ public class SomaFmSongHistory {
public static final String BREAK_STATION_ID = "Break / Station ID";

public static void main(String[] args) {
// Load config
Config config = ConfigFactory.load();
Configuration configuration = ConfigBeanFactory.create(config.getConfig(ROOT_CONFIG), Configuration.class);
String defaultChannel = args.length != 0 ? args[0] : DRONE_ZONE.getPublicName();
RecentBroadcastBusiness business = RecentBroadcastBusiness.init(configuration);
business.displayRecentBroadcasts(defaultChannel);
SomaFmConfig somaFmConfig = ConfigBeanFactory.create(config.getConfig(ROOT_CONFIG), SomaFmConfig.class);

// Prepare database
Flyway.configure()
.dataSource(somaFmConfig.getDbUrl(), somaFmConfig.getDbUser(), somaFmConfig.getDbPassword())
.load()
.migrate();

// Prepare business
RecentBroadcastBusiness business = RecentBroadcastBusiness.init(somaFmConfig);

// Run
new CLI(business).run(args);
}

}
19 changes: 19 additions & 0 deletions src/main/java/net/lecigne/somafm/business/BusinessAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.lecigne.somafm.business;

import java.util.Arrays;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public enum BusinessAction {
DISPLAY("display"), SAVE("save");

private final String actionName;

public static BusinessAction getValue(String actionName) {
return Arrays.stream(BusinessAction.values())
.filter(value -> value.actionName.equals(actionName))
.findFirst()
.orElse(DISPLAY);
}

}
Original file line number Diff line number Diff line change
@@ -1,39 +1,61 @@
package net.lecigne.somafm.business;

import static net.lecigne.somafm.business.BusinessAction.DISPLAY;
import static net.lecigne.somafm.business.BusinessAction.SAVE;

import java.io.IOException;
import java.time.ZoneId;
import java.util.Comparator;
import lombok.RequiredArgsConstructor;
import net.lecigne.somafm.config.Configuration;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import net.lecigne.somafm.config.SomaFmConfig;
import net.lecigne.somafm.exception.SomaFmHtmlParsingException;
import net.lecigne.somafm.mappers.DisplayedBroadcastMapper;
import net.lecigne.somafm.model.Broadcast;
import net.lecigne.somafm.model.Channel;
import net.lecigne.somafm.repository.BroadcastRepository;
import net.lecigne.somafm.repository.DefaultBroadcastRepository;

@RequiredArgsConstructor
@Slf4j
public class RecentBroadcastBusiness {

private final BroadcastRepository broadcastRepository;
private final DisplayedBroadcastMapper displayedBroadcastMapper;
private final Map<BusinessAction, Consumer<Set<Broadcast>>> strategies;

public RecentBroadcastBusiness(
BroadcastRepository broadcastRepository,
DisplayedBroadcastMapper displayedBroadcastMapper) {
this.broadcastRepository = broadcastRepository;
this.displayedBroadcastMapper = displayedBroadcastMapper;
this.strategies = Map.of(DISPLAY, displayRecentBroadcasts(), SAVE, saveRecentBroadcasts());
}

@SuppressWarnings("java:S106") // command line application - println is ok
public void displayRecentBroadcasts(String publicName) {
Channel channel = Channel.getByPublicName(publicName);
public void handleRecentBroadcasts(BusinessAction action, Channel channel) {
try {
broadcastRepository.getRecentBroadcasts(channel).stream()
.sorted(Comparator.comparing(Broadcast::getTime).reversed())
.map(displayedBroadcastMapper::map)
.forEach(System.out::println);
Set<Broadcast> recentBroadcasts = broadcastRepository.getRecentBroadcasts(channel);
strategies.get(action).accept(recentBroadcasts);
} catch (IOException e) {
System.out.println("Unable to get recent broadcast from SomaFM at this time. Please try again later.");
log.error("Unable to get recent broadcasts from SomaFM at this time. Please try again later.", e);
} catch (SomaFmHtmlParsingException e) {
System.out.println(e.getMessage());
log.error("Unable to parse SomaFM recent broadcasts page.", e);
}
}

public static RecentBroadcastBusiness init(Configuration config) {
private Consumer<Set<Broadcast>> saveRecentBroadcasts() {
return broadcastRepository::updateBroadcasts;
}

private Consumer<Set<Broadcast>> displayRecentBroadcasts() {
return broadcasts -> broadcasts.stream()
.sorted(Comparator.comparing(Broadcast::getTime).reversed())
.map(displayedBroadcastMapper::map)
.forEach(System.out::println);
}

public static RecentBroadcastBusiness init(SomaFmConfig config) {
return new RecentBroadcastBusiness(
DefaultBroadcastRepository.init(config),
new DisplayedBroadcastMapper(ZoneId.systemDefault())
Expand Down
Loading

0 comments on commit 085dd3d

Please sign in to comment.