From 7a27e08106de8875d42db7d5a48a3465706e13b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Thomas?= Date: Sat, 4 Jan 2020 19:06:41 +0100 Subject: [PATCH] Add "internal help" page with diagnostic information --- .../controller/InternalHelpController.java | 301 ++++++++++++++++++ .../player/security/GlobalSecurityConfig.java | 2 +- .../player/i18n/ResourceBundle_en.properties | 16 + .../player/theme/default_dark.properties | 2 + .../player/theme/default_light.properties | 2 + .../src/main/webapp/WEB-INF/jsp/help.jsp | 1 + .../main/webapp/WEB-INF/jsp/internalhelp.jsp | 267 ++++++++++++++++ .../main/webapp/icons/default_dark/alert.svg | 1 + .../main/webapp/icons/default_dark/check.svg | 1 + .../main/webapp/icons/default_light/alert.svg | 1 + .../main/webapp/icons/default_light/check.svg | 1 + 11 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 airsonic-main/src/main/java/org/airsonic/player/controller/InternalHelpController.java create mode 100644 airsonic-main/src/main/webapp/WEB-INF/jsp/internalhelp.jsp create mode 100644 airsonic-main/src/main/webapp/icons/default_dark/alert.svg create mode 100644 airsonic-main/src/main/webapp/icons/default_dark/check.svg create mode 100644 airsonic-main/src/main/webapp/icons/default_light/alert.svg create mode 100644 airsonic-main/src/main/webapp/icons/default_light/check.svg diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/InternalHelpController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/InternalHelpController.java new file mode 100644 index 00000000..0cd99a37 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/InternalHelpController.java @@ -0,0 +1,301 @@ +/* + 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.MusicFolderDao; +import org.airsonic.player.domain.MusicFolder; +import org.airsonic.player.service.SecurityService; +import org.airsonic.player.service.SettingsService; +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.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.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 MusicFolderStatistics { + private String name; + private String freeFilesystemSizeBytes; + private String totalFilesystemSizeBytes; + private boolean readable; + private boolean writable; + + public String getName() { + return name; + } + + public String getFreeFilesystemSizeBytes() { + return freeFilesystemSizeBytes; + } + + public boolean isReadable() { + return readable; + } + + public boolean isWritable() { + return writable; + } + + public String getTotalFilesystemSizeBytes() { + return totalFilesystemSizeBytes; + } + + 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 setTotalFilesystemSizeBytes(String totalFilesystemSizeBytes) { + this.totalFilesystemSizeBytes = totalFilesystemSizeBytes; + } + } + + @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; + + @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"); + + // Airsonic scan statistics + map.put("statAlbumCount", indexManager.getStatistics().getAlbumCount()); + map.put("statArtistCount", indexManager.getStatistics().getArtistCount()); + map.put("statSongCount", indexManager.getStatistics().getSongCount()); + map.put("statLastScanDate", indexManager.getStatistics().getScanDate()); + map.put("statTotalDurationSeconds", indexManager.getStatistics().getTotalDurationInSeconds()); + map.put("statTotalLengthBytes", FileUtils.byteCountToDisplaySize(indexManager.getStatistics().getTotalLengthInBytes())); + + // Lucene index statistics + try (IndexReader reader = indexManager.getSearcher(IndexType.SONG).getIndexReader()) { + map.put("indexSongCount", reader.numDocs()); + map.put("indexSongDeletedCount", reader.numDeletedDocs()); + } catch (IOException e) { + LOG.debug("Unable to gather information", e); + } + try (IndexReader reader = indexManager.getSearcher(IndexType.ALBUM).getIndexReader()) { + map.put("indexAlbumCount", reader.numDocs()); + map.put("indexAlbumDeletedCount", reader.numDeletedDocs()); + } catch (IOException e) { + LOG.debug("Unable to gather information", e); + } + try (IndexReader reader = indexManager.getSearcher(IndexType.ARTIST).getIndexReader()) { + map.put("indexArtistCount", reader.numDocs()); + map.put("indexArtistDeletedCount", reader.numDeletedDocs()); + } catch (IOException e) { + LOG.debug("Unable to gather information", e); + } + try (IndexReader reader = indexManager.getSearcher(IndexType.ALBUM_ID3).getIndexReader()) { + map.put("indexAlbumId3Count", reader.numDocs()); + map.put("indexAlbumId3DeletedCount", reader.numDeletedDocs()); + } catch (IOException e) { + LOG.debug("Unable to gather information", e); + } + try (IndexReader reader = indexManager.getSearcher(IndexType.ARTIST_ID3).getIndexReader()) { + map.put("indexArtistId3Count", reader.numDocs()); + map.put("indexArtistId3DeletedCount", reader.numDeletedDocs()); + } catch (IOException e) { + LOG.debug("Unable to gather information", e); + } + try (Analyzer analyzer = analyzerFactory.getAnalyzer()) { + map.put("indexLuceneVersion", analyzer.getVersion().toString()); + } catch (IOException e) { + LOG.debug("Unable to gather information", e); + } + + // Database statistics + try (Connection conn = daoHelper.getDataSource().getConnection()) { + map.put("dbDriverName", conn.getMetaData().getDriverName()); + map.put("dbDriverVersion", conn.getMetaData().getDriverVersion()); + } catch (SQLException e) { + LOG.debug("Unable to gather information", e); + } + 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"))); + SortedMap dbTableCount = new TreeMap<>(); + try { + for (String tableName : daoHelper.getJdbcTemplate().queryForList("SELECT table_name FROM INFORMATION_SCHEMA.SYSTEM_TABLES WHERE table_schem = 'PUBLIC'", String.class)) { + 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); + } + } + } catch (Exception e) { + LOG.debug("Unable to gather information", e); + } + + map.put("dbMediaFileMusicNonPresentCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(*) FROM MEDIA_FILE WHERE NOT present AND type = 'MUSIC'"), Long.class)); + map.put("dbMediaFilePodcastNonPresentCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(*) FROM MEDIA_FILE WHERE NOT present AND type = 'PODCAST'"), Long.class)); + map.put("dbMediaFileDirectoryNonPresentCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(*) FROM MEDIA_FILE WHERE NOT present AND type = 'DIRECTORY'"), Long.class)); + map.put("dbMediaFileAlbumNonPresentCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(*) FROM MEDIA_FILE WHERE NOT present AND type = 'ALBUM'"), Long.class)); + + map.put("dbMediaFileMusicPresentCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(*) FROM MEDIA_FILE WHERE present AND type = 'MUSIC'"), Long.class)); + map.put("dbMediaFilePodcastPresentCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(*) FROM MEDIA_FILE WHERE present AND type = 'PODCAST'"), Long.class)); + map.put("dbMediaFileDirectoryPresentCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(*) FROM MEDIA_FILE WHERE present AND type = 'DIRECTORY'"), Long.class)); + map.put("dbMediaFileAlbumPresentCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(*) FROM MEDIA_FILE WHERE present AND type = 'ALBUM'"), Long.class)); + + map.put("dbMediaFileDistinctAlbumCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(DISTINCT album) FROM MEDIA_FILE WHERE present"), Long.class)); + map.put("dbMediaFileDistinctArtistCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(DISTINCT artist) FROM MEDIA_FILE WHERE present"), Long.class)); + map.put("dbMediaFileDistinctAlbumArtistCount", daoHelper.getJdbcTemplate().queryForObject(String.format("SELECT count(DISTINCT album_artist) FROM MEDIA_FILE WHERE present"), Long.class)); + + map.put("dbTableCount", dbTableCount); + + // Filesystem statistics + 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()) { + MusicFolderStatistics stat = new MusicFolderStatistics(); + stat.setName(folder.getName()); + stat.setFreeFilesystemSizeBytes(FileUtils.byteCountToDisplaySize(folder.getPath().getUsableSpace())); + stat.setTotalFilesystemSizeBytes(FileUtils.byteCountToDisplaySize(folder.getPath().getTotalSpace())); + stat.setReadable(Files.isReadable(folder.getPath().toPath())); + stat.setWritable(Files.isWritable(folder.getPath().toPath())); + fsMusicFolderStatistics.put(folder.getName(), stat); + } + map.put("fsMusicFolderStatistics", fsMusicFolderStatistics); + + // OS information + 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()); + + 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); + + return new ModelAndView("internalhelp","model",map); + } + + 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; + } + } + + +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java b/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java index 6d0cc3d6..8014def7 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java +++ b/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java @@ -167,7 +167,7 @@ public class GlobalSecurityConfig extends GlobalAuthenticationConfigurerAdapter .antMatchers("/personalSettings*", "/passwordSettings*", "/playerSettings*", "/shareSettings*", "/passwordSettings*") .hasRole("SETTINGS") - .antMatchers("/generalSettings*", "/advancedSettings*", "/userSettings*", + .antMatchers("/generalSettings*", "/advancedSettings*", "/userSettings*", "/internalhelp*", "/musicFolderSettings*", "/databaseSettings*", "/transcodeSettings*", "/rest/startScan*") .hasRole("ADMIN") .antMatchers("/deletePlaylist*", "/savePlaylist*") diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties index 3cf7c1d5..e4da26d9 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties @@ -262,6 +262,22 @@ upload.empty=No files to upload. upload.failed=Uploading failed with the following error:
"{0}" upload.unzipped=Unzipped {0} +internalhelp.title=About {0} Internals +internalhelp.details=Internal details +internalhelp.statistics=Statistics +internalhelp.database=Database +internalhelp.index=Search Index +internalhelp.filesystem=Filesystem +internalhelp.musicfolders=Music Folders +internalhelp.locale=Locale +internalhelp.defaultlocale=Default locale +internalhelp.defaultcharset=Default charset encoding +internalhelp.folderisreadable=Folder "{0}": Read access +internalhelp.folderiswritable=Folder "{0}": Write access +internalhelp.folderfsusage=Folder "{0}": Free space on filesystem +internalhelp.fsusage=Airsonic Home: Free space on filesystem +internalhelp.fshomesize=Airsonic Home: Directory size + help.title=About {0} help.upgrade=New version available. Download {0} {1} here. help.version.title=Version diff --git a/airsonic-main/src/main/resources/org/airsonic/player/theme/default_dark.properties b/airsonic-main/src/main/resources/org/airsonic/player/theme/default_dark.properties index c6d2c70b..b608c3c4 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/theme/default_dark.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/theme/default_dark.properties @@ -62,3 +62,5 @@ userImage = icons/default_dark/user.svg viewAsGridImage = icons/default_dark/view_as_grid.png viewAsListImage = icons/default_dark/view_as_list.png volumeImage = icons/default_dark/volume.png +alertImage = icons/default_dark/alert.svg +checkImage = icons/default_dark/check.svg diff --git a/airsonic-main/src/main/resources/org/airsonic/player/theme/default_light.properties b/airsonic-main/src/main/resources/org/airsonic/player/theme/default_light.properties index 6c62f88b..6bfbd11e 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/theme/default_light.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/theme/default_light.properties @@ -62,3 +62,5 @@ userImage = icons/default_light/user.svg viewAsGridImage = icons/default_light/view_as_grid.svg viewAsListImage = icons/default_light/view_as_list.svg volumeImage = icons/default_light/volume.svg +alertImage = icons/default_light/alert.svg +checkImage = icons/default_light/check.svg diff --git a/airsonic-main/src/main/webapp/WEB-INF/jsp/help.jsp b/airsonic-main/src/main/webapp/WEB-INF/jsp/help.jsp index a095eaa5..e5a24638 100644 --- a/airsonic-main/src/main/webapp/WEB-INF/jsp/help.jsp +++ b/airsonic-main/src/main/webapp/WEB-INF/jsp/help.jsp @@ -64,5 +64,6 @@

+
diff --git a/airsonic-main/src/main/webapp/WEB-INF/jsp/internalhelp.jsp b/airsonic-main/src/main/webapp/WEB-INF/jsp/internalhelp.jsp new file mode 100644 index 00000000..87307e49 --- /dev/null +++ b/airsonic-main/src/main/webapp/WEB-INF/jsp/internalhelp.jsp @@ -0,0 +1,267 @@ + +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%> + + + <%@ include file="head.jsp" %> + + + + + + + + + + + + + + + + + + + + + + +

+ + +

+ + +

+
+ + + + + + + + + +
${versionString} – ${buildDateString}
${model.serverInfo} ( / )
+ GPL 3.0 +
Airsonic website
Airsonic on Reddit
+ +

+ +

+ + +

+ + + + + + + + +
statAlbumCount${model.statAlbumCount}
statArtistCount${model.statArtistCount}
statSongCount${model.statSongCount}
statLastScanDate${model.statLastScanDate}
statTotalDurationSeconds${model.statTotalDurationSeconds}
statTotalLengthBytes${model.statTotalLengthBytes}
+ +

+ +

+ + +

+ + + + + + + + + + + + + + + + +
indexLuceneVersion${model.indexLuceneVersion}
indexSongDeletedCount${model.indexSongDeletedCount}
indexAlbumDeletedCount${model.indexAlbumDeletedCount}
indexArtistDeletedCount${model.indexArtistDeletedCount}
indexAlbumId3DeletedCount${model.indexAlbumId3DeletedCount}
indexArtistId3DeletedCount${model.indexArtistId3DeletedCount}
indexSongCount${model.indexSongCount}
indexAlbumCount${model.indexAlbumCount}
indexArtistCount${model.indexArtistCount}
indexAlbumId3Count${model.indexAlbumId3Count}
indexArtistId3Count${model.indexArtistId3Count}
+ +

+ +

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + OK + The database log file (db/airsonic.log) appears healthy. + + + Warning + The database log file (db/airsonic.log) is large (greater than 256M). Run a scan to clean it up. + + +
dbDriverName${model.dbDriverName}
dbDriverVersion${model.dbDriverVersion}
dbDirectorySize${model.dbDirectorySize}
dbLogSize${model.dbLogSize}
+ + + OK + The database does not contain non-present items. + + + Warning + The database contains non-present items. Run "clean-up database" to clean them up. + + +
dbMediaFileMusicNonPresentCount${model.dbMediaFileMusicNonPresentCount}
dbMediaFilePodcastNonPresentCount${model.dbMediaFilePodcastNonPresentCount}
dbMediaFileDirectoryNonPresentCount${model.dbMediaFileDirectoryNonPresentCount}
dbMediaFileAlbumNonPresentCount${model.dbMediaFileAlbumNonPresentCount}
dbMediaFileMusicPresentCount${model.dbMediaFileMusicPresentCount}
dbMediaFilePodcastPresentCount${model.dbMediaFilePodcastPresentCount}
dbMediaFileDirectoryPresentCount${model.dbMediaFileDirectoryPresentCount}
dbMediaFileAlbumPresentCount${model.dbMediaFileAlbumPresentCount}
dbMediaFileDistinctAlbumCount${model.dbMediaFileDistinctAlbumCount}
dbMediaFileDistinctArtistCount${model.dbMediaFileDistinctArtistCount}
dbMediaFileDistinctAlbumArtistCount${model.dbMediaFileDistinctAlbumArtistCount}
${tableCount.key} count${tableCount.value}
+ +

+ +

+ + +

+ + + + + + + + + + + + +
${model.fsHomeUsableSpace} / ${model.fsHomeTotalSpace}
${model.fsHomeDirectorySize}
+ + + OK + Airsonic appears to have the correct permissions for music folder "${musicFolderStatistics.key}". + + + Warning + Airsonic does not have the correct permissions for music folder "${musicFolderStatistics.key}". + + +
${musicFolderStatistics.value.readable}
${musicFolderStatistics.value.writable}
${musicFolderStatistics.value.freeFilesystemSizeBytes} / ${musicFolderStatistics.value.totalFilesystemSizeBytes}
+ +

+ +

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + OK + Java default charset appears to have UTF-8 support. + + + Warning + Java default charset appears to have no UTF-8 support. International characters may be partially supported. + + +
${model.localeDefaultCharset}
${model.localeDefault}
user.language${model.localeUserLanguage}
user.country${model.localeUserCountry}
+ + + OK + Java file encoding appears to have UTF-8 support. + + + Warning + Java file encoding appears to have no UTF-8 support. International characters may be partially supported. + + +
file.encoding${model.localeFileEncoding}
sun.jnu.encoding${model.localeSunJnuEncoding}
sun.io.unicode.encoding${model.localeSunIoUnicodeEncoding}
+ Warning + The LANG environment variable is defined but appears to disable UTF-8 support. International characters may be partially supported. +
LANG${model.localeLang}
+ Warning + The LC_ALL environment variable is defined but appears to disable UTF-8 support. International characters may be partially supported. +
LC_ALL${model.localeLcAll}
+ +

+ +

+ + +

+ + + + + + + +
${fn:escapeXml(entry)}
+ +

+ +
+ + diff --git a/airsonic-main/src/main/webapp/icons/default_dark/alert.svg b/airsonic-main/src/main/webapp/icons/default_dark/alert.svg new file mode 100644 index 00000000..5ceafcd6 --- /dev/null +++ b/airsonic-main/src/main/webapp/icons/default_dark/alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/airsonic-main/src/main/webapp/icons/default_dark/check.svg b/airsonic-main/src/main/webapp/icons/default_dark/check.svg new file mode 100644 index 00000000..ee5e0c98 --- /dev/null +++ b/airsonic-main/src/main/webapp/icons/default_dark/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/airsonic-main/src/main/webapp/icons/default_light/alert.svg b/airsonic-main/src/main/webapp/icons/default_light/alert.svg new file mode 100644 index 00000000..5ceafcd6 --- /dev/null +++ b/airsonic-main/src/main/webapp/icons/default_light/alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/airsonic-main/src/main/webapp/icons/default_light/check.svg b/airsonic-main/src/main/webapp/icons/default_light/check.svg new file mode 100644 index 00000000..ee5e0c98 --- /dev/null +++ b/airsonic-main/src/main/webapp/icons/default_light/check.svg @@ -0,0 +1 @@ + \ No newline at end of file