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.
438 lines
19 KiB
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;
|
|
}
|
|
|
|
}
|
|
|