Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
version: 2
updates:
- package-ecosystem: maven
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
ignore:
- dependency-name: com.google.protobuf:protobuf-java
versions:
- 3.15.0
- 3.15.1
- 3.15.2
- 3.15.4
- 3.15.5
- package-ecosystem: maven
directory: "/lib"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: maven
directory: "/player"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: maven
directory: "/api"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: maven
directory: "/dacp"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: maven
directory: "/sink"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: maven
directory: "/sink-api"
schedule:
interval: daily
open-pull-requests-limit: 10
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ Its main features are:
- Zeroconf (Spotify Connect)
- Gapless playback
- Mixed playlists (cuepoints and transitions)
- DACP metadata pipe
- Execute commands for various events
- Android compatible (see [librespot-android](https://github.com/devgianlu/librespot-android))
- Optional HTTP API (see [librespot-api](api))
- Supports custom sinks and decoders
- Actively developed and up-to-date with the latest internal API

## The library
The `lib` module provides all the necessary components and tools to interact with Spotify. More [here](lib).
Expand All @@ -39,6 +45,7 @@ Snapshots for all variants are available [here](https://oss.sonatype.org/content
- [librespot](https://github.com/librespot-org/librespot)
- [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure librespot-java.
- [spocon](https://github.com/spocon/spocon) - Install librespot-java from APT
- [librespot-android](https://github.com/devgianlu/librespot-android) - Run librespot-java on your Android device

# Special thanks
- All the developers of [librespot](https://github.com/librespot-org/librespot) which started this project in Rust
Expand Down
2 changes: 1 addition & 1 deletion api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<version>2.2.3.Final</version>
<version>2.2.7.Final</version>
</dependency>
</dependencies>
</project>
47 changes: 47 additions & 0 deletions decoder-api/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--
~ Copyright 2021 devgianlu
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>xyz.gianlu.librespot</groupId>
<artifactId>librespot-java</artifactId>
<version>1.5.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>librespot-decoder-api</artifactId>
<packaging>jar</packaging>

<name>librespot-java decoder API</name>

<dependencies>
<dependency>
<groupId>xyz.gianlu.librespot</groupId>
<artifactId>librespot-sink-api</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api.version}</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,34 @@
* limitations under the License.
*/

package xyz.gianlu.librespot.player.codecs;
package xyz.gianlu.librespot.player.decoders;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.gianlu.librespot.audio.AbsChunkedInputStream;
import xyz.gianlu.librespot.audio.GeneralAudioStream;
import xyz.gianlu.librespot.audio.NormalizationData;
import xyz.gianlu.librespot.player.PlayerConfiguration;
import xyz.gianlu.librespot.player.mixing.output.OutputAudioFormat;

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;

/**
* @author Gianlu
* @author devgianlu
*/
public abstract class Codec implements Closeable {
public abstract class Decoder implements Closeable {
public static final int BUFFER_SIZE = 2048;
private static final Logger LOGGER = LoggerFactory.getLogger(Codec.class);
protected final AbsChunkedInputStream audioIn;
private static final Logger LOGGER = LoggerFactory.getLogger(Decoder.class);
protected final SeekableInputStream audioIn;
protected final float normalizationFactor;
protected final int duration;
private final GeneralAudioStream audioFile;
protected volatile boolean closed = false;
protected int seekZero = 0;
private OutputAudioFormat format;

public Codec(@NotNull GeneralAudioStream audioFile, @Nullable NormalizationData normalizationData, @NotNull PlayerConfiguration conf, int duration) {
this.audioIn = audioFile.stream();
this.audioFile = audioFile;
public Decoder(@NotNull SeekableInputStream audioIn, float normalizationFactor, int duration) {
this.audioIn = audioIn;
this.duration = duration;
if (conf.enableNormalisation)
this.normalizationFactor = normalizationData != null ? normalizationData.getFactor(conf.normalisationPregain) : 1;
else
this.normalizationFactor = 1;
this.normalizationFactor = normalizationFactor;
}

public final int writeSomeTo(@NotNull OutputStream out) throws IOException, CodecException {
Expand Down Expand Up @@ -108,25 +98,27 @@ public final int duration() {
return duration;
}

public int size() {
public final int size() {
return audioIn.size();
}

public int decodedLength() {
return audioIn.decodedLength();
}

public int decryptTimeMs() {
return audioFile.decryptTimeMs();
}

public static class CannotGetTimeException extends Exception {
CannotGetTimeException() {
public CannotGetTimeException(String message) {
super(message);
}

public CannotGetTimeException(String message, Throwable cause) {
super(message, cause);
}
}

public static class CodecException extends Exception {
CodecException() {
public CodecException(String message) {
super(message);
}

public CodecException(String message, Throwable cause) {
super(message, cause);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2021 devgianlu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package xyz.gianlu.librespot.player.decoders;

import java.io.IOException;
import java.io.InputStream;

/**
* @author devgianlu
*/
public abstract class SeekableInputStream extends InputStream {
public abstract int size();

public abstract int position();

public abstract void seek(int seekZero) throws IOException;

public abstract long skip(long skip) throws IOException;

public abstract int read(byte[] buffer, int index, int length) throws IOException;

public abstract void close();

public abstract int decodedLength();
}
18 changes: 18 additions & 0 deletions lib/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,24 @@
</build>

<dependencies>
<dependency>
<groupId>xyz.gianlu.librespot</groupId>
<artifactId>librespot-decoder-api</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Audio -->
<dependency>
<groupId>org.jcraft</groupId>
<artifactId>jorbis</artifactId>
<version>0.0.17</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.jlayer</groupId>
<artifactId>jlayer</artifactId>
<version>1.0.2-gdx</version>
</dependency>

<!-- Data -->
<dependency>
<groupId>com.google.protobuf</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
package xyz.gianlu.librespot.audio;

import org.jetbrains.annotations.NotNull;
import xyz.gianlu.librespot.player.decoders.SeekableInputStream;

import java.io.IOException;
import java.io.InputStream;

import static xyz.gianlu.librespot.audio.storage.ChannelManager.CHUNK_SIZE;

/**
* @author Gianlu
* @author devgianlu
*/
public abstract class AbsChunkedInputStream extends InputStream implements HaltListener {
public abstract class AbsChunkedInputStream extends SeekableInputStream implements HaltListener {
private static final int PRELOAD_AHEAD = 3;
private static final int PRELOAD_CHUNK_RETRIES = 2;
private static final int MAX_CHUNK_TRIES = 128;
Expand Down Expand Up @@ -82,10 +82,12 @@ public final synchronized void reset() {
pos = mark;
}

public final synchronized int pos() {
@Override
public final synchronized int position() {
return pos;
}

@Override
public final synchronized void seek(int where) throws IOException {
if (where < 0) throw new IllegalArgumentException();
if (closed) throw new IOException("Stream is closed!");
Expand Down Expand Up @@ -260,6 +262,7 @@ public final void notifyChunkError(int index, @NotNull ChunkException ex) {
}
}

@Override
public int decodedLength() {
return decodedLength;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@


/**
* @author Gianlu
* @author devgianlu
*/
public interface GeneralAudioStream {
public interface DecodedAudioStream {
@NotNull
AbsChunkedInputStream stream();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ private LoadedStream loadEpisode(@NotNull EpisodeId id, @NotNull AudioQualityPic
}
}

private static class FileAudioStream implements GeneralAudioStream {
private static class FileAudioStream implements DecodedAudioStream {
private static final Logger LOGGER = LoggerFactory.getLogger(FileAudioStream.class);
private final File file;
private final RandomAccessFile raf;
Expand Down Expand Up @@ -276,25 +276,25 @@ public int decryptTimeMs() {

public static class LoadedStream {
public final MetadataWrapper metadata;
public final GeneralAudioStream in;
public final DecodedAudioStream in;
public final NormalizationData normalizationData;
public final Metrics metrics;

public LoadedStream(@NotNull Metadata.Track track, @NotNull GeneralAudioStream in, @Nullable NormalizationData normalizationData, @NotNull Metrics metrics) {
public LoadedStream(@NotNull Metadata.Track track, @NotNull DecodedAudioStream in, @Nullable NormalizationData normalizationData, @NotNull Metrics metrics) {
this.metadata = new MetadataWrapper(track, null, null);
this.in = in;
this.normalizationData = normalizationData;
this.metrics = metrics;
}

public LoadedStream(@NotNull Metadata.Episode episode, @NotNull GeneralAudioStream in, @Nullable NormalizationData normalizationData, @NotNull Metrics metrics) {
public LoadedStream(@NotNull Metadata.Episode episode, @NotNull DecodedAudioStream in, @Nullable NormalizationData normalizationData, @NotNull Metrics metrics) {
this.metadata = new MetadataWrapper(null, episode, null);
this.in = in;
this.normalizationData = normalizationData;
this.metrics = metrics;
}

private LoadedStream(@NotNull LocalId id, @NotNull GeneralAudioStream in) {
private LoadedStream(@NotNull LocalId id, @NotNull DecodedAudioStream in) {
this.metadata = new MetadataWrapper(null, null, id);
this.in = in;
this.normalizationData = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ void setUrl(@NotNull HttpUrl url) {
}
}

public class Streamer implements GeneralAudioStream, GeneralWritableStream {
public class Streamer implements DecodedAudioStream, GeneralWritableStream {
private final StreamId streamId;
private final ExecutorService executorService = Executors.newCachedThreadPool(new NameThreadFactory((r) -> "cdn-async-" + r.hashCode()));
private final SuperAudioFormat format;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package xyz.gianlu.librespot.player.codecs;
package xyz.gianlu.librespot.audio.decoders;

import com.spotify.metadata.Metadata.AudioFile;
import org.jetbrains.annotations.NotNull;
Expand Down
Loading