/* 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 . 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.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 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 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 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 map) { SortedMap indexStats = new TreeMap<>(); for (IndexType indexType : IndexType.values()) { try (IndexReader reader = indexManager.getSearcher(indexType).getIndexReader()) { IndexStatistics stat = new IndexStatistics(); stat.setName(indexType.name()); stat.setCount(reader.numDocs()); stat.setDeletedCount(reader.numDeletedDocs()); indexStats.put(indexType.name(), stat); } catch (IOException e) { LOG.debug("Unable to gather information about {} index", indexType.name(), e); } } 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 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 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 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 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 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 map) { map.put("fsFfprobeInfo", gatherStatisticsForTranscodingExecutable("ffprobe")); map.put("fsFfmpegInfo", gatherStatisticsForTranscodingExecutable("ffmpeg")); } private static List getLatestLogEntries(File logFile) { List 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; } }