org.dstadler.audio.fm4.FM4Cache Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of commons-audio Show documentation
Show all versions of commons-audio Show documentation
Common utilities I find useful when developing audio-related projects.
package org.dstadler.audio.fm4;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.dstadler.commons.logging.jdk.LoggerFactory;
import org.dstadler.commons.util.ExecutorUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Simple cache for FM4 shows to avoid delays when switching
* between shows.
*
* The getters() return the currently cached data without performing
* requests to the actual source for FM4 show information.
*
* Automatically refreshes itself in the background every 5
* minutes.
*
* The cache-data is not held in static members, so keep the instance
* available where it is used.
*/
public class FM4Cache implements AutoCloseable {
private final static Logger log = LoggerFactory.make();
// configure cache-implementation so that we keep all matching instances of "FM4Stream"
// per programKey
private final Cache> fm4Cache = CacheBuilder.newBuilder()
// refreshing is done via a scheduled task
.concurrencyLevel(2)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
// we use a thread-pool with one entry to periodically refresh the cache
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(
new BasicThreadFactory.Builder()
.daemon(true)
.namingPattern("FM4Cache-%d")
.uncaughtExceptionHandler((t, e) ->
log.log(Level.WARNING, "Had unexpected exception", e))
.build());
private final FM4 fm4;
private final int days;
/**
* Construct up the cache and start periodic background
* refreshes.
*
* @param fm4 An instance of the FM4 access helper.
* This is passed in to facilitate testing.
* @param days The number of days to cache. Should be between
* 7 and 30 for FM4
*/
public FM4Cache(FM4 fm4, int days) {
this.fm4 = fm4;
this.days = days;
executor.scheduleAtFixedRate(this::refresh, 0, 5, TimeUnit.MINUTES);
}
/**
* Retrieve all found streams for the given programKey.
*
* Multiple streams are usually available for shows which are broadcast
* more than once per week.
*
* @param programKey The FM4-programKey to look for
* @return The resulting list of FM4Stream instances, or null if no entry is found.
*/
public List get(String programKey) {
return fm4Cache.getIfPresent(programKey);
}
public long size() {
return fm4Cache.size();
}
public Collection allStreams() {
List streams = new ArrayList<>();
for (List list : fm4Cache.asMap().values()) {
streams.addAll(list);
}
return streams;
}
/**
* Look for the given URL in all cached FM4Stream instances
* and return the "next" one depending on broadcast-timestamp
*
* @param stream The FM4Stream to look for.
* @return If the url is found, the time-wise next stream is
* returned, null is returned if the url is not found or
* there is no "next" stream, e.g. if the url is from a
* show which is currently broadcast.
*/
public FM4Stream getNext(FM4Stream stream) {
// get a Map of all FM4Streams sorted by start-time
SortedMap streams = getStreamsSortedByTime();
long foundTime = getTimeOfStream(stream);
// no matching URL found
if(foundTime == 0) {
return null;
}
// use foundTime + 1 to not include the current show itself in the result
SortedMap streamsAfter = streams.tailMap(foundTime + 1);
// if this was the last stream the list will be empty
if(streamsAfter.isEmpty()) {
return null;
}
// we found a stream
return streamsAfter.values().iterator().next();
}
/**
* Look for the given URL in all cached FM4Stream instances
* and return the "next" one depending on broadcast-timestamp
*
* @param stream The FM4Stream to look for.
* @return If the url is found, the time-wise next stream is
* returned, null is returned if the url is not found or
* there is no "next" stream, e.g. if the url is from a
* show which is currently broadcast.
*/
public FM4Stream getPrevious(FM4Stream stream) {
// get a Map of all FM4Streams sorted by start-time
SortedMap streams = getStreamsSortedByTime();
long foundTime = getTimeOfStream(stream);
// no matching URL found
if(foundTime == 0) {
return null;
}
// use foundTime - 1 to not include the current show itself in the result
SortedMap streamsAfter = streams.headMap(foundTime - 1);
// if this was the last stream the list will be empty
if(streamsAfter.isEmpty()) {
return null;
}
// we found a stream
return streamsAfter.values().iterator().next();
}
private SortedMap getStreamsSortedByTime() {
SortedMap streams = new TreeMap<>();
for (List fm4Streams : fm4Cache.asMap().values()) {
for (FM4Stream fm4Stream : fm4Streams) {
streams.put(fm4Stream.getStart(), fm4Stream);
}
}
return streams;
}
private long getTimeOfStream(FM4Stream stream) {
long foundTime = 0;
for (List fm4Streams : fm4Cache.asMap().values()) {
for (FM4Stream fm4Stream : fm4Streams) {
// check if this stream contains the URL
if(fm4Stream.equals(stream)) {
log.info("Found current stream: '" + stream.getShortSummary() + "' with start: " + fm4Stream.getStart());
foundTime = fm4Stream.getStart();
}
}
}
return foundTime;
}
/**
* Look for the given URL in all cached FM4Stream instances
* and return the "next" one depending on broadcast-timestamp
*
* @param url The url to look for.
* @return If the url is found, the time-wise next stream is
* returned, null is returned if the url is not found or
* there is no "next" stream, e.g. if the url is from a
* show which is currently broadcast.
*/
public FM4Stream getNextByStreamURL(String url) throws IOException {
// get a Map of all FM4Streams sorted by start-time
SortedMap streams = new TreeMap<>();
long foundTime = 0;
for (List fm4Streams : fm4Cache.asMap().values()) {
for (FM4Stream fm4Stream : fm4Streams) {
streams.put(fm4Stream.getStart(), fm4Stream);
// check if this stream contains the URL
for (String streamUrl : fm4Stream.getStreams()) {
if(streamUrl.equals(url)) {
log.info("Found url " + url + " for stream '" + fm4Stream.getShortSummary() + "' with start: " + fm4Stream.getStart());
foundTime = fm4Stream.getStart();
}
}
}
}
// no matching URL found
if(foundTime == 0) {
return null;
}
// use foundTime + 1 to not include the current show itself in the result
SortedMap streamsAfter = streams.tailMap(foundTime + 1);
// if this was the last stream the list will be empty
if(streamsAfter.isEmpty()) {
return null;
}
// we found a stream
return streamsAfter.values().iterator().next();
}
/**
* Refresh the contents of the cache by re-adding all
* shows found via the FM4 instance provided during
* construction.
*
* Expiry is done by the cache so that non-refreshed
* items are removed from the cache after some time.
*
* Usually there is no need to call this as it is
* called automatically every 5 minutes.
*/
public void refresh() {
try {
// first get all streams by programKey
Multimap streams = ArrayListMultimap.create();
for (FM4Stream stream : fm4.fetchStreams(days)) {
streams.put(stream.getProgramKey(), stream);
}
// then store a list tof items per programKey in the cache
for (String programKey : streams.keySet()) {
fm4Cache.put(programKey, new ArrayList<>(streams.get(programKey)));
}
} catch (IOException e) {
log.log(Level.WARNING, "Failed to read FM4 streams", e);
}
}
@Override
public void close() {
ExecutorUtil.shutdownAndAwaitTermination(executor, 10_000);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy