My fork of airsonic with experimental fixes and improvements. See branch "custom"
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.
 
 
 
airsonic-custom/airsonic-main/src/main/java/org/airsonic/player/controller/InternalHelpController.java

438 lines
19 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.DaoHelper;
import org.airsonic.player.dao.MediaFileDao;
import org.airsonic.player.dao.MusicFolderDao;
import org.airsonic.player.domain.MediaLibraryStatistics;
import org.airsonic.player.domain.MusicFolder;
import org.airsonic.player.service.SecurityService;
import org.airsonic.player.service.SettingsService;
import org.airsonic.player.service.TranscodingService;
import org.airsonic.player.service.VersionService;
import org.airsonic.player.service.search.AnalyzerFactory;
import org.airsonic.player.service.search.IndexManager;
import org.airsonic.player.service.search.IndexType;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.input.ReversedLinesFileReader;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.IndexSearcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* Controller for the help page.
*
* @author Sindre Mehus
*/
@Controller
@RequestMapping("/internalhelp")
public class InternalHelpController {
private static final Logger LOG = LoggerFactory.getLogger(InternalHelpController.class);
private static final int LOG_LINES_TO_SHOW = 50;
public class IndexStatistics {
private String name;
private int count;
private int deletedCount;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getDeletedCount() {
return deletedCount;
}
public void setDeletedCount(int deletedCount) {
this.deletedCount = deletedCount;
}
}
public class FileStatistics {
private String name;
private String path;
private String freeFilesystemSizeBytes;
private String totalFilesystemSizeBytes;
private boolean readable;
private boolean writable;
private boolean executable;
public FileStatistics() {}
public FileStatistics(File path) {
}
public String getName() {
return name;
}
public String getFreeFilesystemSizeBytes() {
return freeFilesystemSizeBytes;
}
public boolean isReadable() {
return readable;
}
public boolean isWritable() {
return writable;
}
public boolean isExecutable() {
return executable;
}
public String getTotalFilesystemSizeBytes() {
return totalFilesystemSizeBytes;
}
public String getPath() {
return path;
}
public void setName(String name) {
this.name = name;
}
public void setFreeFilesystemSizeBytes(String freeFilesystemSizeBytes) {
this.freeFilesystemSizeBytes = freeFilesystemSizeBytes;
}
public void setReadable(boolean readable) {
this.readable = readable;
}
public void setWritable(boolean writable) {
this.writable = writable;
}
public void setExecutable(boolean executable) {
this.executable = executable;
}
public void setTotalFilesystemSizeBytes(String totalFilesystemSizeBytes) {
this.totalFilesystemSizeBytes = totalFilesystemSizeBytes;
}
public void setPath(String path) {
this.path = path;
}
public void setFromFile(File file) {
this.setName(file.getName());
this.setPath(file.getAbsolutePath());
this.setFreeFilesystemSizeBytes(FileUtils.byteCountToDisplaySize(file.getUsableSpace()));
this.setTotalFilesystemSizeBytes(FileUtils.byteCountToDisplaySize(file.getTotalSpace()));
this.setReadable(Files.isReadable(file.toPath()));
this.setWritable(Files.isWritable(file.toPath()));
this.setExecutable(Files.isExecutable(file.toPath()));
}
}
@Autowired
private VersionService versionService;
@Autowired
private SettingsService settingsService;
@Autowired
private SecurityService securityService;
@Autowired
private IndexManager indexManager;
@Autowired
private DaoHelper daoHelper;
@Autowired
private AnalyzerFactory analyzerFactory;
@Autowired
private MusicFolderDao musicFolderDao;
@Autowired
private MediaFileDao mediaFileDao;
@Autowired
private TranscodingService transcodingService;
@Autowired
private Environment environment;
@GetMapping
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
Map<String, Object> map = new HashMap<>();
if (versionService.isNewFinalVersionAvailable()) {
map.put("newVersionAvailable", true);
map.put("latestVersion", versionService.getLatestFinalVersion());
} else if (versionService.isNewBetaVersionAvailable()) {
map.put("newVersionAvailable", true);
map.put("latestVersion", versionService.getLatestBetaVersion());
}
long totalMemory = Runtime.getRuntime().totalMemory();
long freeMemory = Runtime.getRuntime().freeMemory();
String serverInfo = request.getSession().getServletContext().getServerInfo() +
", java " + System.getProperty("java.version") +
", " + System.getProperty("os.name");
map.put("user", securityService.getCurrentUser(request));
map.put("brand", settingsService.getBrand());
map.put("localVersion", versionService.getLocalVersion());
map.put("buildDate", versionService.getLocalBuildDate());
map.put("buildNumber", versionService.getLocalBuildNumber());
map.put("serverInfo", serverInfo);
map.put("usedMemory", totalMemory - freeMemory);
map.put("totalMemory", totalMemory);
File logFile = SettingsService.getLogFile();
List<String> latestLogEntries = getLatestLogEntries(logFile);
map.put("logEntries", latestLogEntries);
map.put("logFile", logFile);
// Gather internal information
gatherScanInfo(map);
gatherIndexInfo(map);
gatherDatabaseInfo(map);
gatherFilesystemInfo(map);
gatherTranscodingInfo(map);
gatherLocaleInfo(map);
return new ModelAndView("internalhelp","model",map);
}
private void gatherScanInfo(Map<String, Object> map) {
// Airsonic scan statistics
MediaLibraryStatistics stats = indexManager.getStatistics();
if (stats != null) {
map.put("statAlbumCount", stats.getAlbumCount());
map.put("statArtistCount", stats.getArtistCount());
map.put("statSongCount", stats.getSongCount());
map.put("statLastScanDate", stats.getScanDate());
map.put("statTotalDurationSeconds", stats.getTotalDurationInSeconds());
map.put("statTotalLengthBytes", FileUtils.byteCountToDisplaySize(stats.getTotalLengthInBytes()));
}
}
private void gatherIndexInfo(Map<String, Object> map) {
SortedMap<String, IndexStatistics> indexStats = new TreeMap<>();
for (IndexType indexType : IndexType.values()) {
IndexStatistics stat = new IndexStatistics();
IndexSearcher searcher = indexManager.getSearcher(indexType);
stat.setName(indexType.name());
indexStats.put(indexType.name(), stat);
if (searcher != null) {
IndexReader reader = searcher.getIndexReader();
stat.setCount(reader.numDocs());
stat.setDeletedCount(reader.numDeletedDocs());
indexManager.release(indexType, searcher);
} else {
stat.setCount(0);
stat.setDeletedCount(0);
}
}
map.put("indexStatistics", indexStats);
try (Analyzer analyzer = analyzerFactory.getAnalyzer()) {
map.put("indexLuceneVersion", analyzer.getVersion().toString());
} catch (IOException e) {
LOG.debug("Unable to gather information", e);
}
}
private void gatherLocaleInfo(Map<String, Object> map) {
map.put("localeDefault", Locale.getDefault());
map.put("localeUserLanguage", System.getProperty("user.language"));
map.put("localeUserCountry", System.getProperty("user.country"));
map.put("localeFileEncoding", System.getProperty("file.encoding"));
map.put("localeSunJnuEncoding", System.getProperty("sun.jnu.encoding"));
map.put("localeSunIoUnicodeEncoding", System.getProperty("sun.io.unicode.encoding"));
map.put("localeLang", System.getenv("LANG"));
map.put("localeLcAll", System.getenv("LC_ALL"));
map.put("localeDefaultCharset", Charset.defaultCharset());
}
private void gatherDatabaseInfo(Map<String, Object> map) {
try (Connection conn = daoHelper.getDataSource().getConnection()) {
// Driver name/version
map.put("dbDriverName", conn.getMetaData().getDriverName());
map.put("dbDriverVersion", conn.getMetaData().getDriverVersion());
map.put("dbServerVersion", conn.getMetaData().getDatabaseProductVersion());
// Gather information for existing database tables
ResultSet resultSet = conn.getMetaData().getTables(null, null, "%", null);
SortedMap<String, Long> dbTableCount = new TreeMap<>();
while (resultSet.next()) {
String tableSchema = resultSet.getString("TABLE_SCHEM");
String tableName = resultSet.getString("TABLE_NAME");
String tableType = resultSet.getString("TABLE_TYPE");
LOG.debug("Got database table {}, schema {}, type {}", tableName, tableSchema, tableType);
if (!"table".equalsIgnoreCase(tableType)) continue; // Table type
// MariaDB has "null" schemas, while other databases use "public".
if (tableSchema != null && !"public".equalsIgnoreCase(tableSchema)) continue; // Table schema
try {
Long tableCount = daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(*) FROM %s", tableName), Long.class);
dbTableCount.put(tableName, tableCount);
} catch (Exception e) {
LOG.debug("Unable to gather information", e);
}
}
map.put("dbTableCount", dbTableCount);
} catch (SQLException e) {
LOG.debug("Unable to gather information", e);
}
if (environment.acceptsProfiles("legacy")) {
map.put("dbIsLegacy", true);
File dbDirectory = new File(SettingsService.getAirsonicHome(), "db");
map.put("dbDirectorySizeBytes", dbDirectory.exists() ? FileUtils.sizeOfDirectory(dbDirectory) : 0);
map.put("dbDirectorySize", FileUtils.byteCountToDisplaySize((long) map.get("dbDirectorySizeBytes")));
File dbLogFile = new File(dbDirectory, "airsonic.log");
map.put("dbLogSizeBytes", dbLogFile.exists() ? dbLogFile.length() : 0);
map.put("dbLogSize", FileUtils.byteCountToDisplaySize((long) map.get("dbLogSizeBytes")));
} else {
map.put("dbIsLegacy", false);
}
map.put("dbMediaFileMusicNonPresentCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(*) FROM media_file WHERE NOT present AND type = 'MUSIC'", Long.class));
map.put("dbMediaFilePodcastNonPresentCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(*) FROM media_file WHERE NOT present AND type = 'PODCAST'", Long.class));
map.put("dbMediaFileDirectoryNonPresentCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(*) FROM media_file WHERE NOT present AND type = 'DIRECTORY'", Long.class));
map.put("dbMediaFileAlbumNonPresentCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(*) FROM media_file wheRE NOT present AND type = 'ALBUM'", Long.class));
map.put("dbMediaFileMusicPresentCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(*) FROM media_file WHERE present AND type = 'MUSIC'", Long.class));
map.put("dbMediaFilePodcastPresentCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(*) FROM media_file WHERE present AND type = 'PODCAST'", Long.class));
map.put("dbMediaFileDirectoryPresentCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(*) FROM media_file WHERE present AND type = 'DIRECTORY'", Long.class));
map.put("dbMediaFileAlbumPresentCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(*) FROM media_file WHERE present AND type = 'ALBUM'", Long.class));
map.put("dbMediaFileDistinctAlbumCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(DISTINCT album) FROM media_file WHERE present", Long.class));
map.put("dbMediaFileDistinctArtistCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(DISTINCT artist) FROM media_file WHERE present", Long.class));
map.put("dbMediaFileDistinctAlbumArtistCount", daoHelper.getJdbcTemplate().queryForObject("SELECT count(DISTINCT album_artist) FROM media_file WHERE present", Long.class));
map.put("dbMediaFilesInNonPresentMusicFoldersCount", mediaFileDao.getFilesInNonPresentMusicFoldersCount(Arrays.asList(settingsService.getPodcastFolder())));
map.put("dbMediaFilesInNonPresentMusicFoldersSample", mediaFileDao.getFilesInNonPresentMusicFolders(10, Arrays.asList(settingsService.getPodcastFolder())));
map.put("dbMediaFilesWithMusicFolderMismatchCount", mediaFileDao.getFilesWithMusicFolderMismatchCount());
map.put("dbMediaFilesWithMusicFolderMismatchSample", mediaFileDao.getFilesWithMusicFolderMismatch(10));
}
private void gatherFilesystemInfo(Map<String, Object> map) {
map.put("fsHomeDirectorySizeBytes", FileUtils.sizeOfDirectory(SettingsService.getAirsonicHome()));
map.put("fsHomeDirectorySize", FileUtils.byteCountToDisplaySize((long)map.get("fsHomeDirectorySizeBytes")));
map.put("fsHomeTotalSpaceBytes", SettingsService.getAirsonicHome().getTotalSpace());
map.put("fsHomeTotalSpace", FileUtils.byteCountToDisplaySize((long)map.get("fsHomeTotalSpaceBytes")));
map.put("fsHomeUsableSpaceBytes", SettingsService.getAirsonicHome().getUsableSpace());
map.put("fsHomeUsableSpace", FileUtils.byteCountToDisplaySize((long)map.get("fsHomeUsableSpaceBytes")));
SortedMap<String, FileStatistics> fsMusicFolderStatistics = new TreeMap<>();
for (MusicFolder folder: musicFolderDao.getAllMusicFolders()) {
FileStatistics stat = new FileStatistics();
stat.setFromFile(folder.getPath());
stat.setName(folder.getName());
fsMusicFolderStatistics.put(folder.getName(), stat);
}
map.put("fsMusicFolderStatistics", fsMusicFolderStatistics);
}
private void gatherTranscodingInfo(Map<String, Object> map) {
map.put("fsFfprobeInfo", gatherStatisticsForTranscodingExecutable("ffprobe"));
map.put("fsFfmpegInfo", gatherStatisticsForTranscodingExecutable("ffmpeg"));
}
private static List<String> getLatestLogEntries(File logFile) {
List<String> lines = new ArrayList<>(LOG_LINES_TO_SHOW);
try (ReversedLinesFileReader reader = new ReversedLinesFileReader(logFile, Charset.defaultCharset())) {
String current;
while ((current = reader.readLine()) != null) {
if (lines.size() >= LOG_LINES_TO_SHOW) {
break;
}
lines.add(0, current);
}
return lines;
} catch (IOException e) {
LOG.warn("Could not open log file " + logFile, e);
return null;
}
}
private File lookForExecutable(String executableName) {
for (String path : System.getenv("PATH").split(File.pathSeparator)) {
File file = new File(path, executableName);
if (file.exists()) {
LOG.debug("Found {} in {}", executableName, path);
return file;
} else {
LOG.debug("Looking for {} in {} (not found)", executableName, path);
}
}
return null;
}
private File lookForTranscodingExecutable(String executableName) {
File executableLocation = null;
for (String name: Arrays.asList(executableName, String.format("%s.exe", executableName))) {
executableLocation = new File(transcodingService.getTranscodeDirectory(), name);
if (executableLocation != null && executableLocation.exists()) return executableLocation;
executableLocation = lookForExecutable(executableName);
if (executableLocation != null && executableLocation.exists()) return executableLocation;
}
return null;
}
private FileStatistics gatherStatisticsForTranscodingExecutable(String executableName) {
FileStatistics executableStatistics = null;
File executableLocation = lookForTranscodingExecutable(executableName);
if (executableLocation != null) {
executableStatistics = new FileStatistics();
executableStatistics.setFromFile(executableLocation);
}
return executableStatistics;
}
}