You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
733 lines
26 KiB
733 lines
26 KiB
/*
|
|
This file is part of Airsonic.
|
|
|
|
Airsonic is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Airsonic is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
Copyright 2016 (C) Airsonic Authors
|
|
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
|
|
*/
|
|
package org.airsonic.player.controller;
|
|
|
|
import org.airsonic.player.dao.AlbumDao;
|
|
import org.airsonic.player.dao.ArtistDao;
|
|
import org.airsonic.player.domain.*;
|
|
import org.airsonic.player.service.*;
|
|
import org.airsonic.player.service.metadata.JaudiotaggerParser;
|
|
import org.airsonic.player.util.FileUtil;
|
|
import org.airsonic.player.util.StringUtil;
|
|
import org.apache.commons.codec.digest.DigestUtils;
|
|
import org.apache.commons.io.FilenameUtils;
|
|
import org.apache.commons.io.IOUtils;
|
|
import org.apache.commons.lang3.tuple.Pair;
|
|
import org.jaudiotagger.tag.images.Artwork;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.stereotype.Controller;
|
|
import org.springframework.web.bind.ServletRequestUtils;
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
import org.springframework.web.servlet.mvc.LastModified;
|
|
|
|
import javax.annotation.PostConstruct;
|
|
import javax.imageio.ImageIO;
|
|
import javax.servlet.http.HttpServletRequest;
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
|
import java.awt.*;
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.*;
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.concurrent.Semaphore;
|
|
|
|
/**
|
|
* Controller which produces cover art images.
|
|
*
|
|
* @author Sindre Mehus
|
|
*/
|
|
@Controller
|
|
@RequestMapping({"/coverArt", "/ext/coverArt"})
|
|
public class CoverArtController implements LastModified {
|
|
|
|
public static final String ALBUM_COVERART_PREFIX = "al-";
|
|
public static final String ARTIST_COVERART_PREFIX = "ar-";
|
|
public static final String PLAYLIST_COVERART_PREFIX = "pl-";
|
|
public static final String PODCAST_COVERART_PREFIX = "pod-";
|
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(CoverArtController.class);
|
|
|
|
@Autowired
|
|
private MediaFileService mediaFileService;
|
|
@Autowired
|
|
private TranscodingService transcodingService;
|
|
@Autowired
|
|
private SettingsService settingsService;
|
|
@Autowired
|
|
private PlaylistService playlistService;
|
|
@Autowired
|
|
private PodcastService podcastService;
|
|
@Autowired
|
|
private ArtistDao artistDao;
|
|
@Autowired
|
|
private AlbumDao albumDao;
|
|
@Autowired
|
|
private JaudiotaggerParser jaudiotaggerParser;
|
|
private Semaphore semaphore;
|
|
|
|
@PostConstruct
|
|
public void init() {
|
|
semaphore = new Semaphore(settingsService.getCoverArtConcurrency());
|
|
}
|
|
|
|
public long getLastModified(HttpServletRequest request) {
|
|
CoverArtRequest coverArtRequest = createCoverArtRequest(request);
|
|
// LOG.info("getLastModified - " + coverArtRequest + ": " + new Date(result));
|
|
return coverArtRequest.lastModified();
|
|
}
|
|
|
|
@GetMapping
|
|
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
|
|
|
CoverArtRequest coverArtRequest = createCoverArtRequest(request);
|
|
LOG.trace("handleRequest - " + coverArtRequest);
|
|
Integer size = ServletRequestUtils.getIntParameter(request, "size");
|
|
|
|
// Send fallback image if no ID is given. (No need to cache it, since it will be cached in browser.)
|
|
if (coverArtRequest == null) {
|
|
sendFallback(size, response);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Optimize if no scaling is required.
|
|
if (size == null && coverArtRequest.getCoverArt() != null) {
|
|
LOG.trace("sendUnscaled - " + coverArtRequest);
|
|
sendUnscaled(coverArtRequest, response);
|
|
return;
|
|
}
|
|
|
|
// Send cached image, creating it if necessary.
|
|
if (size == null) {
|
|
size = CoverArtScheme.LARGE.getSize() * 2;
|
|
}
|
|
File cachedImage = getCachedImage(coverArtRequest, size);
|
|
sendImage(cachedImage, response);
|
|
} catch (Exception e) {
|
|
LOG.debug("Sending fallback as an exception was encountered during normal cover art processing", e);
|
|
sendFallback(size, response);
|
|
}
|
|
|
|
}
|
|
|
|
private CoverArtRequest createCoverArtRequest(HttpServletRequest request) {
|
|
String id = request.getParameter("id");
|
|
if (id == null) {
|
|
return null;
|
|
}
|
|
|
|
if (id.startsWith(ALBUM_COVERART_PREFIX)) {
|
|
return createAlbumCoverArtRequest(Integer.valueOf(id.replace(ALBUM_COVERART_PREFIX, "")));
|
|
}
|
|
if (id.startsWith(ARTIST_COVERART_PREFIX)) {
|
|
return createArtistCoverArtRequest(Integer.valueOf(id.replace(ARTIST_COVERART_PREFIX, "")));
|
|
}
|
|
if (id.startsWith(PLAYLIST_COVERART_PREFIX)) {
|
|
return createPlaylistCoverArtRequest(Integer.valueOf(id.replace(PLAYLIST_COVERART_PREFIX, "")));
|
|
}
|
|
if (id.startsWith(PODCAST_COVERART_PREFIX)) {
|
|
return createPodcastCoverArtRequest(Integer.valueOf(id.replace(PODCAST_COVERART_PREFIX, "")), request);
|
|
}
|
|
return createMediaFileCoverArtRequest(Integer.valueOf(id), request);
|
|
}
|
|
|
|
private CoverArtRequest createAlbumCoverArtRequest(int id) {
|
|
Album album = albumDao.getAlbum(id);
|
|
return album == null ? null : new AlbumCoverArtRequest(album);
|
|
}
|
|
|
|
private CoverArtRequest createArtistCoverArtRequest(int id) {
|
|
Artist artist = artistDao.getArtist(id);
|
|
return artist == null ? null : new ArtistCoverArtRequest(artist);
|
|
}
|
|
|
|
private PlaylistCoverArtRequest createPlaylistCoverArtRequest(int id) {
|
|
Playlist playlist = playlistService.getPlaylist(id);
|
|
return playlist == null ? null : new PlaylistCoverArtRequest(playlist);
|
|
}
|
|
|
|
private CoverArtRequest createPodcastCoverArtRequest(int id, HttpServletRequest request) {
|
|
PodcastChannel channel = podcastService.getChannel(id);
|
|
if (channel == null) {
|
|
return null;
|
|
}
|
|
if (channel.getMediaFileId() == null) {
|
|
return new PodcastCoverArtRequest(channel);
|
|
}
|
|
return createMediaFileCoverArtRequest(channel.getMediaFileId(), request);
|
|
}
|
|
|
|
private CoverArtRequest createMediaFileCoverArtRequest(int id, HttpServletRequest request) {
|
|
MediaFile mediaFile = mediaFileService.getMediaFile(id);
|
|
if (mediaFile == null) {
|
|
return null;
|
|
}
|
|
if (mediaFile.isVideo()) {
|
|
int offset = ServletRequestUtils.getIntParameter(request, "offset", 60);
|
|
return new VideoCoverArtRequest(mediaFile, offset);
|
|
}
|
|
return new MediaFileCoverArtRequest(mediaFile);
|
|
}
|
|
|
|
private void sendImage(File file, HttpServletResponse response) throws IOException {
|
|
response.setContentType(StringUtil.getMimeType(FilenameUtils.getExtension(file.getName())));
|
|
InputStream in = new FileInputStream(file);
|
|
try {
|
|
IOUtils.copy(in, response.getOutputStream());
|
|
} finally {
|
|
FileUtil.closeQuietly(in);
|
|
}
|
|
}
|
|
|
|
private void sendFallback(Integer size, HttpServletResponse response) throws IOException {
|
|
if (response.getContentType() == null) {
|
|
response.setContentType(StringUtil.getMimeType("jpeg"));
|
|
}
|
|
InputStream in = null;
|
|
try {
|
|
in = getClass().getResourceAsStream("default_cover.jpg");
|
|
BufferedImage image = ImageIO.read(in);
|
|
if (size != null) {
|
|
image = scale(image, size, size);
|
|
}
|
|
ImageIO.write(image, "jpeg", response.getOutputStream());
|
|
} finally {
|
|
FileUtil.closeQuietly(in);
|
|
}
|
|
}
|
|
|
|
private void sendUnscaled(CoverArtRequest coverArtRequest, HttpServletResponse response) throws IOException {
|
|
File file = coverArtRequest.getCoverArt();
|
|
InputStream in = null;
|
|
try {
|
|
Pair<InputStream, String> imageInputStreamWithType = getImageInputStreamWithType(file);
|
|
in = imageInputStreamWithType.getLeft();
|
|
response.setContentType(imageInputStreamWithType.getRight());
|
|
IOUtils.copy(in, response.getOutputStream());
|
|
} finally {
|
|
FileUtil.closeQuietly(in);
|
|
}
|
|
}
|
|
|
|
private File getCachedImage(CoverArtRequest request, int size) throws IOException {
|
|
String hash = DigestUtils.md5Hex(request.getKey());
|
|
String encoding = request.getCoverArt() != null ? "jpeg" : "png";
|
|
File cachedImage = new File(getImageCacheDirectory(size), hash + "." + encoding);
|
|
|
|
// Synchronize to avoid concurrent writing to the same file.
|
|
synchronized (hash.intern()) {
|
|
|
|
// Is cache missing or obsolete?
|
|
if (!cachedImage.exists() || request.lastModified() > cachedImage.lastModified()) {
|
|
// LOG.info("Cache MISS - " + request + " (" + size + ")");
|
|
OutputStream out = null;
|
|
try {
|
|
semaphore.acquire();
|
|
BufferedImage image = request.createImage(size);
|
|
if (image == null) {
|
|
throw new Exception("Unable to decode image.");
|
|
}
|
|
out = new FileOutputStream(cachedImage);
|
|
ImageIO.write(image, encoding, out);
|
|
|
|
} catch (Throwable x) {
|
|
// Delete corrupt (probably empty) thumbnail cache.
|
|
LOG.warn("Failed to create thumbnail for " + request, x);
|
|
FileUtil.closeQuietly(out);
|
|
cachedImage.delete();
|
|
throw new IOException("Failed to create thumbnail for " + request + ". " + x.getMessage());
|
|
|
|
} finally {
|
|
semaphore.release();
|
|
FileUtil.closeQuietly(out);
|
|
}
|
|
} else {
|
|
// LOG.info("Cache HIT - " + request + " (" + size + ")");
|
|
}
|
|
return cachedImage;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an input stream to the image in the given file. If the file is an audio file,
|
|
* the embedded album art is returned.
|
|
*/
|
|
private InputStream getImageInputStream(File file) throws IOException {
|
|
return getImageInputStreamWithType(file).getLeft();
|
|
}
|
|
|
|
/**
|
|
* Returns an input stream to the image in the given file. If the file is an audio file,
|
|
* the embedded album art is returned. In addition returns the mime type
|
|
*/
|
|
private Pair<InputStream, String> getImageInputStreamWithType(File file) throws IOException {
|
|
InputStream is;
|
|
String mimeType;
|
|
if (jaudiotaggerParser.isApplicable(file)) {
|
|
LOG.trace("Using Jaudio Tagger for reading artwork from {}", file);
|
|
MediaFile mediaFile = mediaFileService.getMediaFile(file);
|
|
Artwork artwork;
|
|
try {
|
|
LOG.trace("Reading artwork from file {}", mediaFile);
|
|
artwork = jaudiotaggerParser.getArtwork(mediaFile);
|
|
is = new ByteArrayInputStream(artwork.getBinaryData());
|
|
mimeType = artwork.getMimeType();
|
|
} catch (Exception e) {
|
|
LOG.debug("Could not read artwork from file {}", mediaFile);
|
|
throw new RuntimeException(e);
|
|
}
|
|
} else {
|
|
is = new FileInputStream(file);
|
|
mimeType = StringUtil.getMimeType(FilenameUtils.getExtension(file.getName()));
|
|
}
|
|
return Pair.of(is, mimeType);
|
|
}
|
|
|
|
private InputStream getImageInputStreamForVideo(MediaFile mediaFile, int width, int height, int offset) throws Exception {
|
|
VideoTranscodingSettings videoSettings = new VideoTranscodingSettings(width, height, offset, 0, false);
|
|
TranscodingService.Parameters parameters = new TranscodingService.Parameters(mediaFile, videoSettings);
|
|
String command = settingsService.getVideoImageCommand();
|
|
parameters.setTranscoding(new Transcoding(null, null, null, null, command, null, null, false));
|
|
return transcodingService.getTranscodedInputStream(parameters);
|
|
}
|
|
|
|
private synchronized File getImageCacheDirectory(int size) {
|
|
File dir = new File(SettingsService.getAirsonicHome(), "thumbs");
|
|
dir = new File(dir, String.valueOf(size));
|
|
if (!dir.exists()) {
|
|
if (dir.mkdirs()) {
|
|
LOG.info("Created thumbnail cache " + dir);
|
|
} else {
|
|
LOG.error("Failed to create thumbnail cache " + dir);
|
|
}
|
|
}
|
|
|
|
return dir;
|
|
}
|
|
|
|
public static BufferedImage scale(BufferedImage image, int width, int height) {
|
|
int w = image.getWidth();
|
|
int h = image.getHeight();
|
|
BufferedImage thumb = image;
|
|
|
|
// For optimal results, use step by step bilinear resampling - halfing the size at each step.
|
|
do {
|
|
w /= 2;
|
|
h /= 2;
|
|
if (w < width) {
|
|
w = width;
|
|
}
|
|
if (h < height) {
|
|
h = height;
|
|
}
|
|
|
|
BufferedImage temp = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
|
Graphics2D g2 = temp.createGraphics();
|
|
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
|
|
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
|
g2.drawImage(thumb, 0, 0, temp.getWidth(), temp.getHeight(), null);
|
|
g2.dispose();
|
|
|
|
thumb = temp;
|
|
} while (w != width);
|
|
|
|
return thumb;
|
|
}
|
|
|
|
|
|
private abstract class CoverArtRequest {
|
|
|
|
protected File coverArt;
|
|
|
|
private CoverArtRequest() {
|
|
}
|
|
|
|
private CoverArtRequest(String coverArtPath) {
|
|
this.coverArt = coverArtPath == null ? null : new File(coverArtPath);
|
|
}
|
|
|
|
private File getCoverArt() {
|
|
return coverArt;
|
|
}
|
|
|
|
public abstract String getKey();
|
|
|
|
public abstract long lastModified();
|
|
|
|
public BufferedImage createImage(int size) {
|
|
if (coverArt != null) {
|
|
InputStream in = null;
|
|
String reason = null;
|
|
try {
|
|
in = getImageInputStream(coverArt);
|
|
if (in == null) {
|
|
reason = "getImageInputStream";
|
|
} else {
|
|
BufferedImage bimg = ImageIO.read(in);
|
|
if (bimg == null) {
|
|
reason = "ImageIO.read";
|
|
} else {
|
|
return scale(bimg, size, size);
|
|
}
|
|
}
|
|
LOG.warn("Failed to process cover art " + coverArt + ": " + reason + " failed");
|
|
} catch (Throwable x) {
|
|
LOG.warn("Failed to process cover art " + coverArt + ": " + x, x);
|
|
} finally {
|
|
FileUtil.closeQuietly(in);
|
|
}
|
|
}
|
|
return createAutoCover(size, size);
|
|
}
|
|
|
|
protected BufferedImage createAutoCover(int width, int height) {
|
|
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
Graphics2D graphics = image.createGraphics();
|
|
AutoCover autoCover = new AutoCover(graphics, getKey(), getArtist(), getAlbum(), width, height);
|
|
autoCover.paintCover();
|
|
graphics.dispose();
|
|
return image;
|
|
}
|
|
|
|
public abstract String getAlbum();
|
|
|
|
public abstract String getArtist();
|
|
}
|
|
|
|
private class ArtistCoverArtRequest extends CoverArtRequest {
|
|
|
|
private final Artist artist;
|
|
|
|
private ArtistCoverArtRequest(Artist artist) {
|
|
super(artist.getCoverArtPath());
|
|
this.artist = artist;
|
|
}
|
|
|
|
@Override
|
|
public String getKey() {
|
|
return artist.getCoverArtPath() != null ? artist.getCoverArtPath() : (ARTIST_COVERART_PREFIX + artist.getId());
|
|
}
|
|
|
|
@Override
|
|
public long lastModified() {
|
|
return coverArt != null ? coverArt.lastModified() : artist.getLastScanned().getTime();
|
|
}
|
|
|
|
@Override
|
|
public String getAlbum() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public String getArtist() {
|
|
return artist.getName();
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Artist " + artist.getId() + " - " + artist.getName();
|
|
}
|
|
}
|
|
|
|
private class AlbumCoverArtRequest extends CoverArtRequest {
|
|
|
|
private final Album album;
|
|
|
|
private AlbumCoverArtRequest(Album album) {
|
|
super(album.getCoverArtPath());
|
|
this.album = album;
|
|
}
|
|
|
|
@Override
|
|
public String getKey() {
|
|
return album.getCoverArtPath() != null ? album.getCoverArtPath() : (ALBUM_COVERART_PREFIX + album.getId());
|
|
}
|
|
|
|
@Override
|
|
public long lastModified() {
|
|
return coverArt != null ? coverArt.lastModified() : album.getLastScanned().getTime();
|
|
}
|
|
|
|
@Override
|
|
public String getAlbum() {
|
|
return album.getName();
|
|
}
|
|
|
|
@Override
|
|
public String getArtist() {
|
|
return album.getArtist();
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Album " + album.getId() + " - " + album.getName();
|
|
}
|
|
}
|
|
|
|
private class PlaylistCoverArtRequest extends CoverArtRequest {
|
|
|
|
private final Playlist playlist;
|
|
|
|
private PlaylistCoverArtRequest(Playlist playlist) {
|
|
super(null);
|
|
this.playlist = playlist;
|
|
}
|
|
|
|
@Override
|
|
public String getKey() {
|
|
return PLAYLIST_COVERART_PREFIX + playlist.getId();
|
|
}
|
|
|
|
@Override
|
|
public long lastModified() {
|
|
return playlist.getChanged().getTime();
|
|
}
|
|
|
|
@Override
|
|
public String getAlbum() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public String getArtist() {
|
|
return playlist.getName();
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Playlist " + playlist.getId() + " - " + playlist.getName();
|
|
}
|
|
|
|
@Override
|
|
public BufferedImage createImage(int size) {
|
|
List<MediaFile> albums = getRepresentativeAlbums();
|
|
if (albums.isEmpty()) {
|
|
return createAutoCover(size, size);
|
|
}
|
|
if (albums.size() < 4) {
|
|
return new MediaFileCoverArtRequest(albums.get(0)).createImage(size);
|
|
}
|
|
|
|
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
|
|
Graphics2D graphics = image.createGraphics();
|
|
|
|
int half = size / 2;
|
|
graphics.drawImage(new MediaFileCoverArtRequest(albums.get(0)).createImage(half), null, 0, 0);
|
|
graphics.drawImage(new MediaFileCoverArtRequest(albums.get(1)).createImage(half), null, half, 0);
|
|
graphics.drawImage(new MediaFileCoverArtRequest(albums.get(2)).createImage(half), null, 0, half);
|
|
graphics.drawImage(new MediaFileCoverArtRequest(albums.get(3)).createImage(half), null, half, half);
|
|
graphics.dispose();
|
|
return image;
|
|
}
|
|
|
|
private List<MediaFile> getRepresentativeAlbums() {
|
|
Set<MediaFile> albums = new LinkedHashSet<MediaFile>();
|
|
for (MediaFile song : playlistService.getFilesInPlaylist(playlist.getId())) {
|
|
MediaFile album = mediaFileService.getParentOf(song);
|
|
if (album != null && !mediaFileService.isRoot(album)) {
|
|
albums.add(album);
|
|
}
|
|
}
|
|
return new ArrayList<>(albums);
|
|
}
|
|
}
|
|
|
|
private class PodcastCoverArtRequest extends CoverArtRequest {
|
|
|
|
private final PodcastChannel channel;
|
|
|
|
PodcastCoverArtRequest(PodcastChannel channel) {
|
|
this.channel = channel;
|
|
}
|
|
|
|
@Override
|
|
public String getKey() {
|
|
return PODCAST_COVERART_PREFIX + channel.getId();
|
|
}
|
|
|
|
@Override
|
|
public long lastModified() {
|
|
return -1;
|
|
}
|
|
|
|
@Override
|
|
public String getAlbum() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public String getArtist() {
|
|
return channel.getTitle() != null ? channel.getTitle() : channel.getUrl();
|
|
}
|
|
}
|
|
|
|
private class MediaFileCoverArtRequest extends CoverArtRequest {
|
|
|
|
private final MediaFile mediaFile;
|
|
private final MediaFile dir;
|
|
|
|
private MediaFileCoverArtRequest(MediaFile mediaFile) {
|
|
this.mediaFile = mediaFile;
|
|
dir = mediaFile.isDirectory() ? mediaFile : mediaFileService.getParentOf(mediaFile);
|
|
coverArt = mediaFileService.getCoverArt(mediaFile);
|
|
}
|
|
|
|
@Override
|
|
public String getKey() {
|
|
return coverArt != null ? coverArt.getPath() : dir.getPath();
|
|
}
|
|
|
|
@Override
|
|
public long lastModified() {
|
|
return coverArt != null ? coverArt.lastModified() : dir.getChanged().getTime();
|
|
}
|
|
|
|
@Override
|
|
public String getAlbum() {
|
|
return dir.getName();
|
|
}
|
|
|
|
@Override
|
|
public String getArtist() {
|
|
return dir.getAlbumArtist() != null ? dir.getAlbumArtist() : dir.getArtist();
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Media file " + mediaFile.getId() + " - " + mediaFile;
|
|
}
|
|
}
|
|
|
|
private class VideoCoverArtRequest extends CoverArtRequest {
|
|
|
|
private final MediaFile mediaFile;
|
|
private final int offset;
|
|
|
|
private VideoCoverArtRequest(MediaFile mediaFile, int offset) {
|
|
this.mediaFile = mediaFile;
|
|
this.offset = offset;
|
|
}
|
|
|
|
@Override
|
|
public BufferedImage createImage(int size) {
|
|
int height;
|
|
height = size;
|
|
int width = height * 16 / 9;
|
|
InputStream in = null;
|
|
try {
|
|
in = getImageInputStreamForVideo(mediaFile, width, height, offset);
|
|
BufferedImage result = ImageIO.read(in);
|
|
if (result != null) {
|
|
return result;
|
|
}
|
|
LOG.warn("Failed to process cover art for " + mediaFile + ": {}", result);
|
|
} catch (Throwable x) {
|
|
LOG.warn("Failed to process cover art for " + mediaFile + ": " + x, x);
|
|
} finally {
|
|
FileUtil.closeQuietly(in);
|
|
}
|
|
return createAutoCover(width, height);
|
|
}
|
|
|
|
@Override
|
|
public String getKey() {
|
|
return mediaFile.getPath() + "/" + offset;
|
|
}
|
|
|
|
@Override
|
|
public long lastModified() {
|
|
return mediaFile.getChanged().getTime();
|
|
}
|
|
|
|
@Override
|
|
public String getAlbum() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public String getArtist() {
|
|
return mediaFile.getName();
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Video file " + mediaFile.getId() + " - " + mediaFile;
|
|
}
|
|
}
|
|
|
|
static class AutoCover {
|
|
|
|
private final static int[] COLORS = {0x33B5E5, 0xAA66CC, 0x99CC00, 0xFFBB33, 0xFF4444};
|
|
private final Graphics2D graphics;
|
|
private final String artist;
|
|
private final String album;
|
|
private final int width;
|
|
private final int height;
|
|
private final Color color;
|
|
|
|
AutoCover(Graphics2D graphics, String key, String artist, String album, int width, int height) {
|
|
this.graphics = graphics;
|
|
this.artist = artist;
|
|
this.album = album;
|
|
this.width = width;
|
|
this.height = height;
|
|
|
|
int hash = key.hashCode();
|
|
int rgb = COLORS[Math.abs(hash) % COLORS.length];
|
|
this.color = new Color(rgb);
|
|
}
|
|
|
|
public void paintCover() {
|
|
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
|
|
|
graphics.setPaint(color);
|
|
graphics.fillRect(0, 0, width, height);
|
|
|
|
int y = height * 2 / 3;
|
|
graphics.setPaint(new GradientPaint(0, y, new Color(82, 82, 82), 0, height, Color.BLACK));
|
|
graphics.fillRect(0, y, width, height / 3);
|
|
|
|
graphics.setPaint(Color.WHITE);
|
|
float fontSize = 3.0f + height * 0.07f;
|
|
Font font = new Font(Font.SANS_SERIF, Font.BOLD, (int) fontSize);
|
|
graphics.setFont(font);
|
|
|
|
if (album != null) {
|
|
graphics.drawString(album, width * 0.05f, height * 0.6f);
|
|
}
|
|
if (artist != null) {
|
|
graphics.drawString(artist, width * 0.05f, height * 0.8f);
|
|
}
|
|
|
|
int borderWidth = height / 50;
|
|
graphics.fillRect(0, 0, borderWidth, height);
|
|
graphics.fillRect(width - borderWidth, 0, height - borderWidth, height);
|
|
graphics.fillRect(0, 0, width, borderWidth);
|
|
graphics.fillRect(0, height - borderWidth, width, height);
|
|
}
|
|
}
|
|
}
|
|
|