From 50419af87468396dbbe5a5f2d9f10a2646de8cda Mon Sep 17 00:00:00 2001 From: Michael Skogberg Date: Sun, 19 Nov 2023 22:20:03 +0200 Subject: [PATCH] Try to release --- .github/workflows/ci.yml | 12 +- README.md | 1 - build.sbt | 76 +- musicmeta/README.md | 18 - .../com/malliina/concurrent/Execution.scala | 7 - .../app/com/malliina/http/DiscoClient.scala | 154 --- .../app/com/malliina/http/exceptions.scala | 5 - musicmeta/app/com/malliina/http/models.scala | 15 - .../com/malliina/musicmeta/AppLoader.scala | 85 -- .../com/malliina/musicmeta/BuildMeta.scala | 12 - .../app/com/malliina/musicmeta/MetaHtml.scala | 148 --- .../com/malliina/musicmeta/UserFeedback.scala | 39 - musicmeta/app/controllers/Covers.scala | 78 -- musicmeta/app/controllers/LogStreamer.scala | 48 - musicmeta/app/controllers/MetaAssets.scala | 7 - musicmeta/app/controllers/MetaOAuth.scala | 54 - .../app/controllers/MetaOAuthControl.scala | 41 - musicmeta/conf/application.conf | 11 - musicmeta/conf/logback.xml | 29 - musicmeta/conf/routes | 11 - musicmeta/frontend/css/custom.less | 38 - musicmeta/frontend/css/footer.less | 23 - musicmeta/frontend/css/musicmeta.js | 1 - musicmeta/frontend/css/musicmeta.less | 3 - .../frontend/css/open-iconic-bootstrap.less | 960 ------------------ musicmeta/frontend/fonts/open-iconic.eot | Bin 28196 -> 0 bytes musicmeta/frontend/fonts/open-iconic.otf | Bin 20996 -> 0 bytes musicmeta/frontend/fonts/open-iconic.svg | 543 ---------- musicmeta/frontend/fonts/open-iconic.ttf | Bin 28028 -> 0 bytes musicmeta/frontend/fonts/open-iconic.woff | Bin 14984 -> 0 bytes musicmeta/frontend/postcss.config.js | 8 - .../malliina/musicmeta/js/MetaFrontend.scala | 28 - .../malliina/musicmeta/js/MetaSocket.scala | 101 -- .../com/malliina/musicmeta/js/models.scala | 22 - .../com/malliina/musicmeta/js/package.scala | 19 - musicmeta/frontend/webpack.base.config.js | 34 - musicmeta/frontend/webpack.dev.config.js | 6 - musicmeta/frontend/webpack.prod.config.js | 6 - musicmeta/src/pkg/musicmeta.conf | 1 - musicmeta/src/pkg/unix/changelog | 1 - musicmeta/src/pkg/unix/control/postinstall.sh | 40 - .../src/pkg/unix/control/postuninstall.sh | 5 - musicmeta/src/pkg/unix/control/preinstall.sh | 4 - .../src/pkg/unix/control/preuninstall.sh | 18 - musicmeta/src/pkg/unix/copyright | 1 - musicmeta/src/pkg/unix/lib/.empty.txt | 0 musicmeta/src/pkg/unix/logs/.empty.txt | 0 musicmeta/src/pkg/unix/musicmeta.defaults | 7 - musicmeta/src/pkg/unix/musicmeta.sh | 113 --- musicmeta/test/tests/APITests.scala | 74 -- musicmeta/test/tests/AppTests.scala | 26 - musicmeta/test/tests/DiscoGsTests.scala | 35 - musicmeta/test/tests/LogStreamTests.scala | 14 - musicmeta/test/tests/OtherTests.scala | 11 - project/build.properties | 2 +- project/plugins.sbt | 6 +- 56 files changed, 22 insertions(+), 2979 deletions(-) delete mode 100644 musicmeta/README.md delete mode 100644 musicmeta/app/com/malliina/concurrent/Execution.scala delete mode 100644 musicmeta/app/com/malliina/http/DiscoClient.scala delete mode 100644 musicmeta/app/com/malliina/http/exceptions.scala delete mode 100644 musicmeta/app/com/malliina/http/models.scala delete mode 100644 musicmeta/app/com/malliina/musicmeta/AppLoader.scala delete mode 100644 musicmeta/app/com/malliina/musicmeta/BuildMeta.scala delete mode 100644 musicmeta/app/com/malliina/musicmeta/MetaHtml.scala delete mode 100644 musicmeta/app/com/malliina/musicmeta/UserFeedback.scala delete mode 100644 musicmeta/app/controllers/Covers.scala delete mode 100644 musicmeta/app/controllers/LogStreamer.scala delete mode 100644 musicmeta/app/controllers/MetaAssets.scala delete mode 100644 musicmeta/app/controllers/MetaOAuth.scala delete mode 100644 musicmeta/app/controllers/MetaOAuthControl.scala delete mode 100644 musicmeta/conf/application.conf delete mode 100644 musicmeta/conf/logback.xml delete mode 100644 musicmeta/conf/routes delete mode 100644 musicmeta/frontend/css/custom.less delete mode 100644 musicmeta/frontend/css/footer.less delete mode 100644 musicmeta/frontend/css/musicmeta.js delete mode 100644 musicmeta/frontend/css/musicmeta.less delete mode 100644 musicmeta/frontend/css/open-iconic-bootstrap.less delete mode 100755 musicmeta/frontend/fonts/open-iconic.eot delete mode 100755 musicmeta/frontend/fonts/open-iconic.otf delete mode 100755 musicmeta/frontend/fonts/open-iconic.svg delete mode 100755 musicmeta/frontend/fonts/open-iconic.ttf delete mode 100755 musicmeta/frontend/fonts/open-iconic.woff delete mode 100644 musicmeta/frontend/postcss.config.js delete mode 100644 musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/MetaFrontend.scala delete mode 100644 musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/MetaSocket.scala delete mode 100644 musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/models.scala delete mode 100644 musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/package.scala delete mode 100644 musicmeta/frontend/webpack.base.config.js delete mode 100644 musicmeta/frontend/webpack.dev.config.js delete mode 100644 musicmeta/frontend/webpack.prod.config.js delete mode 100644 musicmeta/src/pkg/musicmeta.conf delete mode 100644 musicmeta/src/pkg/unix/changelog delete mode 100644 musicmeta/src/pkg/unix/control/postinstall.sh delete mode 100644 musicmeta/src/pkg/unix/control/postuninstall.sh delete mode 100644 musicmeta/src/pkg/unix/control/preinstall.sh delete mode 100644 musicmeta/src/pkg/unix/control/preuninstall.sh delete mode 100644 musicmeta/src/pkg/unix/copyright delete mode 100644 musicmeta/src/pkg/unix/lib/.empty.txt delete mode 100644 musicmeta/src/pkg/unix/logs/.empty.txt delete mode 100644 musicmeta/src/pkg/unix/musicmeta.defaults delete mode 100644 musicmeta/src/pkg/unix/musicmeta.sh delete mode 100644 musicmeta/test/tests/APITests.scala delete mode 100644 musicmeta/test/tests/AppTests.scala delete mode 100644 musicmeta/test/tests/DiscoGsTests.scala delete mode 100644 musicmeta/test/tests/LogStreamTests.scala delete mode 100644 musicmeta/test/tests/OtherTests.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f6df74..2b9ed89b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Test +name: Package and release on: push @@ -16,7 +16,13 @@ jobs: cache: sbt - name: Compile run: sbt Test/compile - - name: Build deb package + - name: Build deb packages run: | sudo apt-get install -y lintian - sbt "set scalaJSStage in Global := FullOptStage" musicpimp/ciBuild musicmeta/ciBuild pimpcloud/ciBuild + sbt "set scalaJSStage in Global := FullOptStage" musicpimp/Debian/packageBin pimpcloud/Debian/packageBin + - name: Release .deb packages + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/v') + with: + files: | + */*.deb diff --git a/README.md b/README.md index 28e9733e..c5f66bf3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ This is the MusicPimp server software for Windows/Linux desktops. Check [www.mus - [musicpimp](musicpimp) as a standalone app - [pimpbeam](pimpbeam) deployed to [beam.musicpimp.org](https://beam.musicpimp.org) -- [musicmeta](musicmeta) deployed to [api.musicpimp.org](https://api.musicpimp.org) - [pimpcloud](pimpcloud) deployed to [cloud.musicpimp.org](https://cloud.musicpimp.org) ## Development diff --git a/build.sbt b/build.sbt index e3ada1c5..82b050c2 100644 --- a/build.sbt +++ b/build.sbt @@ -1,10 +1,10 @@ import java.nio.file.{Files, Path, Paths, StandardCopyOption} import com.malliina.appbundler.FileMapping import com.malliina.sbt.GenericKeys._ -import com.malliina.sbt.filetree.DirMap +import com.malliina.filetree.DirMap import com.malliina.sbt.mac.MacKeys._ import com.malliina.sbt.mac.MacPlugin.{Mac, macSettings} -import com.malliina.sbt.unix.LinuxKeys.{appHome, ciBuild, httpPort, httpsPort} +import com.malliina.sbt.unix.LinuxKeys.{ciBuild, httpPort, httpsPort} import com.malliina.sbt.unix.{LinuxPlugin => LinusPlugin} import com.malliina.sbt.win.WinKeys.{minJavaVersion, msiMappings, useTerminateProcess, winSwExe} import com.malliina.sbt.win.{WinKeys, WinPlugin} @@ -158,67 +158,6 @@ val it = project ) ) -val metaCommonSettings = Seq( - version := "1.12.0", - scalacOptions := Seq("-unchecked", "-deprecation") -) -val musicmetaFrontend = scalajsProject("musicmeta-frontend", file("musicmeta") / "frontend") - .settings(metaCommonSettings) - .settings( - libraryDependencies ++= Seq( - "com.lihaoyi" %%% "scalatags" % scalaTagsVersion, - "be.doeraene" %%% "scalajs-jquery" % "1.0.0", - "com.typesafe.play" %%% "play-json" % playJsonVersion, - "com.malliina" %%% "primitives" % primitivesVersion - ), - Compile / npmDependencies ++= Seq("jquery" -> "3.3.1") - ) -val musicmeta = project - .in(file("musicmeta")) - .enablePlugins( - PlayScala, - JavaServerAppPackaging, - SystemdPlugin, - BuildInfoPlugin, - FileTreePlugin, - WebScalaJSBundlerPlugin - ) - .settings(serverSettings ++ metaCommonSettings) - .settings( - scalaJSProjects := Seq(musicmetaFrontend), - Assets / pipelineStages := Seq(scalaJSPipeline), - libraryDependencies ++= Seq( - "commons-codec" % "commons-codec" % "1.15", - logstreamsDep, - malliinaGroup %% "play-social" % utilPlayVersion, - utilPlayDep, - utilPlayDep % Test classifier "tests" - ), - Linux / httpPort := Option("disabled"), - Linux / httpsPort := Option("8460"), - maintainer := "Michael Skogberg ", - Universal / javaOptions ++= { - val linuxName = (Linux / name).value - val metaHome = (Linux / appHome).value - Seq( - s"-Ddiscogs.oauth=/etc/$linuxName/discogs-oauth.key", - s"-Dgoogle.oauth=/etc/$linuxName/google-oauth.key", - s"-Dcover.dir=$metaHome/covers", - s"-Dconfig.file=/etc/$linuxName/production.conf", - s"-Dlogger.file=/etc/$linuxName/logback-prod.xml", - "-Dfile.encoding=UTF-8", - "-Dsun.jnu.encoding=UTF-8", - s"-Dpidfile.path=/dev/null", - ) - }, - pipelineStages := Seq(digest, gzip), - buildInfoKeys ++= Seq[BuildInfoKey]( - "frontName" -> (musicmetaFrontend / name).value - ), - buildInfoPackage := "com.malliina.musicmeta", - linuxPackageSymlinks := linuxPackageSymlinks.value.filterNot(_.link == "/usr/bin/starter") - ) - val pimpbeam = project .in(file("pimpbeam")) .enablePlugins( @@ -251,7 +190,7 @@ val pimpbeam = project buildInfoPackage := "com.malliina.beam" ) -val pimp = project.in(file(".")).aggregate(musicpimp, pimpcloud, musicmeta, pimpbeam) +val pimp = project.in(file(".")).aggregate(musicpimp, pimpcloud, pimpbeam) addCommandAlias("pimp", ";project musicpimp") addCommandAlias("cloud", ";project pimpcloud") @@ -291,11 +230,14 @@ lazy val pimpPlaySettings = ), fileTreeSources := Seq( DirMap( - (Assets / resourceDirectory).value, + (Assets / resourceDirectory).value.toPath, "com.malliina.musicpimp.assets.AppAssets", "com.malliina.musicpimp.html.PimpHtml.at" ), - DirMap((Compile / resourceDirectory).value, "com.malliina.musicpimp.licenses.LicenseFiles") + DirMap( + (Compile / resourceDirectory).value.toPath, + "com.malliina.musicpimp.licenses.LicenseFiles" + ) ), libs := libs.value.filter { lib => !lib.toFile.getAbsolutePath @@ -419,7 +361,7 @@ lazy val pimpcloudSettings = PlayKeys.externalizeResources := false, fileTreeSources := Seq( DirMap( - (Assets / resourceDirectory).value, + (Assets / resourceDirectory).value.toPath, "com.malliina.pimpcloud.assets.CloudAssets", "controllers.pimpcloud.CloudTags.at" ) diff --git a/musicmeta/README.md b/musicmeta/README.md deleted file mode 100644 index 022dce1e..00000000 --- a/musicmeta/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# musicmeta - -This is an album cover HTTP service. - -## API - -To get an album cover image, send an HTTP request to - - /covers?artist=$artist&album=$album - -If the cover is found, a 200 OK is returned along with the cover file. If the cover cannot be found, a 404 NOT FOUND is -returned. If the request is erroneous, a 400 BAD REQUEST is returned. - -## Implementation - -DiscoGs is used as the cover backend. Covers are cached on the local filesystem in a directory specified by system -property ```cover.dir```. DiscoGs requires OAuth authentication, so you need to specify OAuth credentials in a file -specified by system property ```discogs.oauth```. diff --git a/musicmeta/app/com/malliina/concurrent/Execution.scala b/musicmeta/app/com/malliina/concurrent/Execution.scala deleted file mode 100644 index 4160be56..00000000 --- a/musicmeta/app/com/malliina/concurrent/Execution.scala +++ /dev/null @@ -1,7 +0,0 @@ -package com.malliina.concurrent - -import scala.concurrent.ExecutionContext - -object Execution { - implicit val cached: ExecutionContext = com.malliina.web.Execution.cached -} diff --git a/musicmeta/app/com/malliina/http/DiscoClient.scala b/musicmeta/app/com/malliina/http/DiscoClient.scala deleted file mode 100644 index 060d63ac..00000000 --- a/musicmeta/app/com/malliina/http/DiscoClient.scala +++ /dev/null @@ -1,154 +0,0 @@ -package com.malliina.http - -import java.io.Closeable -import java.nio.file.{Files, Path} -import com.malliina.concurrent.Execution -import com.malliina.http.DiscoClient.{DiscoGsCredentials, keys, log} -import com.malliina.storage._ -import com.malliina.util.WebUtils.encodeURIComponent -import org.apache.commons.codec.digest.DigestUtils -import play.api.Logger -import play.api.http.HeaderNames.AUTHORIZATION -import play.api.libs.json.{JsValue, Json} -import com.malliina.play.util.ConfOps -import com.malliina.values.ErrorMessage -import play.api.Configuration - -import scala.concurrent.{ExecutionContext, Future} - -object DiscoClient { - private val log = Logger(getClass) - - case class DiscoGsCredentials(token: String) - - object DiscoGsCredentials { - def apply(conf: Configuration): Either[ErrorMessage, DiscoGsCredentials] = - for { - token <- conf.read("discogs.access.token") - } yield DiscoGsCredentials(token) - } - - object keys { - val CoverImage = "cover_image" - val Id = "id" - val Images = "images" - val Results = "results" - val Uri = "uri" - } - - def apply(creds: DiscoGsCredentials, coverDir: Path): DiscoClient = - new DiscoClient(creds, coverDir)(Execution.cached) -} - -class DiscoClient(credentials: DiscoGsCredentials, coverDir: Path)( - implicit ec: ExecutionContext -) extends Closeable { - Files.createDirectories(coverDir) - val httpClient = OkClient.default - val iLoveDiscoGsFakeCoverSize = 15378 - - /** Returns the album cover. Optionally downloads and caches it if it doesn't already exist locally. - * - * Fails with a [[NoSuchElementException]] if the cover cannot be found. Can also fail with a [[java.io.IOException]] - * and a [[com.fasterxml.jackson.core.JsonParseException]]. - * - * @return the album cover file, which is an image - */ - def cover(artist: String, album: String): Future[Path] = { - val file = coverFile(artist, album) - if (Files.isReadable(file) && Files.size(file) != iLoveDiscoGsFakeCoverSize) - Future.successful(file) - else downloadCover(artist, album).filter(f => Files.size(f) != iLoveDiscoGsFakeCoverSize) - } - - def downloadCover(artist: String, album: String): Future[Path] = - downloadCover(artist, album, _ => coverFile(artist, album)) - - /** Streams `url` to `file`. - * - * @param url url to download - * @param file destination path - * @return the size of the downloaded file, stored in `file` - * @see http://www.playframework.com/documentation/2.6.x/ScalaWS - */ - protected def downloadFile(url: FullUrl, file: Path): Future[StorageSize] = { - httpClient.download(url, file, Map(AUTHORIZATION -> authValue)).flatMap { either => - either.fold( - err => Future.failed(new ResponseException(err)), - s => Future.successful(s) - ) - } - } - - protected def coverFile(artist: String, album: String): Path = { - // avoids platform-specific file system encoding nonsense - val hash = DigestUtils.md5Hex(s"$artist-$album") - coverDir resolve s"$hash.jpg" - } - - /** Downloads the album cover of `artist`s `album`. - * - * Performs three web requests in sequence to the DiscoGs API: - * - * 1) Obtains the album ID - * 2) Obtains the album details (with the given album ID) - * 3) Downloads the album cover (the URL of which is available in the details) - * - * At least the last step, which downloads the cover, requires OAuth authentication. - * - * @param artist the artist - * @param album the album - * @param fileFor the file to download the cover to, given its remote URL - * @return the downloaded album cover along with the number of bytes downloaded - */ - protected def downloadCover( - artist: String, - album: String, - fileFor: FullUrl => Path - ): Future[Path] = - for { - url <- albumCoverForSearch(albumIdUrl(artist, album)) - file = fileFor(url) - _ <- downloadFile(url, file) - } yield file - - private def albumCoverForSearch(url: FullUrl): Future[FullUrl] = - getResponse(url).map { r => - coverImageForResult(Json.parse(r.asString)) - .getOrElse( - throw new CoverNotFoundException( - s"Unable to find cover image from response: '${r.asString}'." - ) - ) - } - - private def getResponse(url: FullUrl): Future[HttpResponse] = authenticated(url) - .flatMap(r => validate(r, url).fold(Future.successful(r))(Future.failed)) - - private def authenticated(url: FullUrl) = { - log debug s"Preparing authenticated request to '$url'..." - httpClient.get(url, Map(AUTHORIZATION -> authValue)) - } - - private def albumIdUrl(artist: String, album: String): FullUrl = { - val artistEnc = encodeURIComponent(artist) - val albumEnc = encodeURIComponent(album) - FullUrl.https("api.discogs.com", s"/database/search?artist=$artistEnc&release_title=$albumEnc") - } - - private def validate(wsResponse: OkHttpResponse, url: FullUrl): Option[Exception] = { - val code = wsResponse.code - code match { - case c if (c >= 200 && c < 300) || c == 404 => None - case _ => Option(new ResponseException(StatusError(wsResponse, url))) - } - } - - private def coverImageForResult(json: JsValue): Option[FullUrl] = { - (json \ keys.Results \\ keys.CoverImage).headOption.flatMap(_.asOpt[FullUrl]) - } - - private def authValue = s"Discogs token=${credentials.token}" - - def close(): Unit = httpClient.close() -} diff --git a/musicmeta/app/com/malliina/http/exceptions.scala b/musicmeta/app/com/malliina/http/exceptions.scala deleted file mode 100644 index 3cd25e44..00000000 --- a/musicmeta/app/com/malliina/http/exceptions.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.malliina.http - -class CoverNotFoundException(msg: String) extends MusicException(msg) - -class MusicException(msg: String) extends Exception(msg) diff --git a/musicmeta/app/com/malliina/http/models.scala b/musicmeta/app/com/malliina/http/models.scala deleted file mode 100644 index abb1845a..00000000 --- a/musicmeta/app/com/malliina/http/models.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.malliina.http - -import play.api.libs.json.{Json, OFormat} - -case class SingleError(message: String) - -object SingleError { - implicit val json: OFormat[SingleError] = Json.format[SingleError] -} - -case class Errors(errors: Seq[SingleError]) - -object Errors { - implicit val json: OFormat[Errors] = Json.format[Errors] -} diff --git a/musicmeta/app/com/malliina/musicmeta/AppLoader.scala b/musicmeta/app/com/malliina/musicmeta/AppLoader.scala deleted file mode 100644 index d4deff0c..00000000 --- a/musicmeta/app/com/malliina/musicmeta/AppLoader.scala +++ /dev/null @@ -1,85 +0,0 @@ -package com.malliina.musicmeta - -import com.malliina.http.DiscoClient.DiscoGsCredentials - -import java.nio.file.Paths -import com.malliina.oauth.GoogleOAuthCredentials -import com.malliina.play.ActorExecution -import com.malliina.play.app.DefaultApp -import com.typesafe.config.ConfigFactory -import controllers._ -import play.api.ApplicationLoader.Context -import play.api.http.HttpConfiguration -import play.api.routing.Router -import play.api.{BuiltInComponentsFromContext, Configuration} -import play.filters.HttpFiltersComponents -import play.filters.headers.SecurityHeadersConfig -import play.filters.hosts.AllowedHostsConfig -import router.Routes - -import scala.concurrent.Future - -object LocalConf { - val localConfFile = Paths.get(sys.props("user.home")).resolve(".musicmeta/musicmeta.conf") - val localConf = Configuration(ConfigFactory.parseFile(localConfFile.toFile)) -} - -class AppLoader extends DefaultApp(AppComponents.prod) - -object AppComponents { - def prod(ctx: Context) = new AppComponents( - ctx, - c => DiscoGsCredentials(c).fold(err => throw new Exception(err.message), identity), - c => GoogleOAuthCredentials(c).fold(err => throw new Exception(err.message), identity) - ) -} - -class AppComponents( - context: Context, - disco: Configuration => DiscoGsCredentials, - google: Configuration => GoogleOAuthCredentials -) extends BuiltInComponentsFromContext(context) - with HttpFiltersComponents - with AssetsComponents { - override val configuration: Configuration = - LocalConf.localConf.withFallback(context.initialConfiguration) - private val allowedCsp = Seq( - "*.musicpimp.org", - "*.bootstrapcdn.com", - "*.googleapis.com", - "code.jquery.com", - "use.fontawesome.com", - "cdnjs.cloudflare.com" - ) - private val allowedEntry = allowedCsp.mkString(" ") - - private val csp = - s"default-src 'self' 'unsafe-inline' 'unsafe-eval' $allowedEntry data:; connect-src *; img-src 'self' data:;" - override lazy val securityHeadersConfig = SecurityHeadersConfig( - contentSecurityPolicy = Option(csp) - ) - override lazy val allowedHostsConfig = AllowedHostsConfig(Seq("localhost", "api.musicpimp.org")) - - val defaultHttpConf = HttpConfiguration.fromConfiguration(configuration, environment) - // Sets sameSite = None, otherwise the Google auth redirect will wipe out the session state - override lazy val httpConfiguration = - defaultHttpConf.copy( - session = defaultHttpConf.session.copy(cookieName = "metaSession", sameSite = None) - ) - - lazy val oauthControl = - new MetaOAuthControl(controllerComponents.actionBuilder, google(configuration)) - lazy val exec = ActorExecution(actorSystem, materializer) - lazy val oauth = MetaOAuth( - "username", - MetaHtml(BuildInfo.frontName, environment.mode), - defaultActionBuilder, - exec - ) - lazy val covers = new Covers(oauth, disco(configuration), controllerComponents) - lazy val metaAssets = new MetaAssets(assets) - override val router: Router = - new Routes(httpErrorHandler, oauth, oauthControl, covers, metaAssets) - - applicationLifecycle.addStopHook { () => Future.successful(oauthControl.http.close()) } -} diff --git a/musicmeta/app/com/malliina/musicmeta/BuildMeta.scala b/musicmeta/app/com/malliina/musicmeta/BuildMeta.scala deleted file mode 100644 index 0042904e..00000000 --- a/musicmeta/app/com/malliina/musicmeta/BuildMeta.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.malliina.musicmeta - -import play.api.libs.json.{Json, OFormat} - -case class BuildMeta(name: String, version: String, scalaVersion: String, gitHash: String) - -object BuildMeta { - implicit val json: OFormat[BuildMeta] = Json.format[BuildMeta] - - def default = - BuildMeta(BuildInfo.name, BuildInfo.version, BuildInfo.scalaVersion, BuildInfo.gitHash) -} diff --git a/musicmeta/app/com/malliina/musicmeta/MetaHtml.scala b/musicmeta/app/com/malliina/musicmeta/MetaHtml.scala deleted file mode 100644 index b465dcbb..00000000 --- a/musicmeta/app/com/malliina/musicmeta/MetaHtml.scala +++ /dev/null @@ -1,148 +0,0 @@ -package com.malliina.musicmeta - -import com.malliina.html.{Bootstrap, HtmlTags} -import com.malliina.play.tags.TagPage -import controllers.routes -import controllers.routes.MetaAssets.versioned -import play.api.Mode -import play.api.mvc.Call -import scalatags.Text.GenericAttr -import scalatags.Text.all._ - -case class ScalaScripts(jsFiles: Seq[String]) - -object ScalaScripts { - - /** - * @param appName typically the name of the Scala.js module - * @param isProd true if the app runs in production, false otherwise - */ - def forApp(appName: String, isProd: Boolean): ScalaScripts = { - val name = appName.toLowerCase - val opt = if (isProd) "opt" else "fastopt" - ScalaScripts(Seq(s"$name-$opt-library.js", s"$name-$opt-loader.js", s"$name-$opt.js")) - } -} - -object MetaHtml { - def apply(appName: String, mode: Mode): MetaHtml = - new MetaHtml(ScalaScripts.forApp(appName, mode == Mode.Prod)) -} - -class MetaHtml(scripts: ScalaScripts) extends Bootstrap(HtmlTags) { - - import tags._ - - implicit val callAttr: GenericAttr[Call] = new GenericAttr[Call] - val empty: Modifier = () - - def logs(feedback: Option[UserFeedback]) = baseIndex("logs", wide = true)( - headerRow("Logs"), - fullRow( - feedback.fold(empty)(feedbackDiv), - span(id := "status", `class` := Lead)("Initializing..."), - divClass( - s"${btn.group} btn-group-toggle compact-group float-right", - role := "group", - data("toggle") := "buttons" - )( - label(`class` := s"${btn.info} ${btn.sm}", id := "label-verbose")( - input( - `type` := "radio", - name := "options", - id := "option-verbose", - autocomplete := "off" - )(" Verbose") - ), - label(`class` := s"${btn.info} ${btn.sm} active", id := "label-compact")( - input( - `type` := "radio", - name := "options", - id := "option-compact", - autocomplete := "off" - )(" Compact") - ) - ) - ), - fullRow( - table(`class` := tables.defaultClass)( - thead(tr(th("Time"), th("Message"), th(`class` := "verbose off")("Logger"), th("Level"))), - tbody(id := "log-table-body") - ) - ), - scripts.jsFiles.map { file => jsScript(versioned(file)) } - ) - - def eject(feedback: Option[UserFeedback]) = - basePage("Goodbye!")( - divContainer( - halfRow( - feedback.fold(empty)(feedbackDiv), - p("Try to ", a(href := routes.MetaOAuth.logs)("sign in"), " again.") - ) - ) - ) - - def baseIndex(tabName: String, wide: Boolean)(content: Modifier*) = { - def navItem(thisTabName: String, tabId: String, url: Call, iconicName: String) = { - val itemClass = if (tabId == tabName) "nav-item active" else "nav-item" - li(`class` := itemClass)( - a(href := url, `class` := "nav-link")(iconic(iconicName), s" $thisTabName") - ) - } - - basePage("MusicPimp")( - navbar.basic( - routes.MetaOAuth.index, - "musicmeta", - modifier( - ulClass(s"${navbars.Nav} $MrAuto")( - navItem("Logs", "logs", routes.MetaOAuth.logs, "list") - ), - ulClass(s"${navbars.Nav} ${navbars.Right}")( - li(`class` := "nav-item")( - a(href := routes.MetaOAuth.logout, `class` := "nav-link")("Logout") - ) - ) - ) - ), - (if (wide) divClass("wide-content") else divContainer)(content) - ) - } - - def basePage(title: String)(content: Modifier*) = TagPage( - html(lang := En)( - head( - titleTag(title), - deviceWidthViewport, - cssLinkHashed( - "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css", - "sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" - ), - cssLink("https://use.fontawesome.com/releases/v5.0.6/css/all.css"), - cssLink(versioned("styles.css")) - ), - body( - content, - footer(`class` := "footer")( - divClass(Container)( - spanClass(s"${text.muted} float-right")( - "Developed by ", - a(href := "https://github.com/malliina")("Michael Skogberg"), - "." - ) - ) - ) - ) - ) - ) - - def feedbackDiv(feedback: UserFeedback) = { - val message = feedback.message - if (feedback.isError) alertDanger(message) - else alertSuccess(message) - } - - def iconic(iconicName: String) = - spanClass(s"oi oi-$iconicName", title := iconicName, aria.hidden := True) -} diff --git a/musicmeta/app/com/malliina/musicmeta/UserFeedback.scala b/musicmeta/app/com/malliina/musicmeta/UserFeedback.scala deleted file mode 100644 index ad8acd69..00000000 --- a/musicmeta/app/com/malliina/musicmeta/UserFeedback.scala +++ /dev/null @@ -1,39 +0,0 @@ -package com.malliina.musicmeta - -import com.malliina.musicmeta.UserFeedback.{Feedback, No, Success, Yes} -import play.api.data.Form -import play.api.mvc.{Flash, RequestHeader} - -case class UserFeedback(message: String, isError: Boolean) { - def flash: Flash = Flash( - Map( - Feedback -> message, - Success -> (if (isError) No else Yes) - ) - ) -} - -object UserFeedback { - val Feedback = "feedback" - val Success = "success" - val Yes = "yes" - val No = "no" - - def success(message: String) = UserFeedback(message, isError = false) - - def error(message: String) = UserFeedback(message, isError = true) - - def flashed(request: RequestHeader): Option[UserFeedback] = - flashed(request.flash) - - def flashed(flash: Flash, textKey: String = Feedback): Option[UserFeedback] = - for { - message <- flash get textKey - isError = (flash get Success) contains No - } yield UserFeedback(message, isError) - - def formed(form: Form[_]) = - form.globalError - .orElse(form.errors.headOption) - .map(formError => error(formError.message)) -} diff --git a/musicmeta/app/controllers/Covers.scala b/musicmeta/app/controllers/Covers.scala deleted file mode 100644 index 5b2c6a19..00000000 --- a/musicmeta/app/controllers/Covers.scala +++ /dev/null @@ -1,78 +0,0 @@ -package controllers - -import java.net.ConnectException -import java.nio.file.Paths -import com.malliina.concurrent.Execution.cached -import com.malliina.http.DiscoClient.DiscoGsCredentials -import com.malliina.http._ -import com.malliina.oauth.DiscoGsOAuthCredentials -import com.malliina.play.http.Proxies -import controllers.Covers.log -import play.api.Logger -import play.api.libs.json.Json -import play.api.mvc._ - -import scala.concurrent.Future - -object Covers { - private val log = Logger(getClass) - - val tempDir = Paths.get(sys.props("java.io.tmpdir")) -} - -class Covers(oauth: MetaOAuth, creds: DiscoGsCredentials, comps: ControllerComponents) - extends AbstractController(comps) { - val fallbackCoverDir = Covers.tempDir.resolve("covers") - val coverDir = sys.props.get("cover.dir").fold(fallbackCoverDir)(path => Paths.get(path)) - val covers = DiscoClient(creds, coverDir) - - def ping = oauth.logged(Action(Ok)) - - def cover = oauth.logged { - Action.async { request => - def message(msg: String) = s"From '${Proxies.realAddress(request)}': $msg" - - def query(key: String) = (request getQueryString key).filter(_.nonEmpty) - - val result = for { - artist <- query("artist") - album <- query("album") - } yield { - val coverName = s"$artist - $album" - covers - .cover(artist, album) - .map { path => - log info message(s"Serving cover '$coverName' at '$path'.") - Ok.sendFile(path.toFile) - } - .recover { - case _: CoverNotFoundException => - val userMessage = s"Unable to find cover '$coverName'." - log info message(userMessage) - notFound(userMessage) - case _: NoSuchElementException => - val userMessage = s"Unable to find cover '$coverName'." - log info message(userMessage) - notFound(userMessage) - case re: ResponseException => - log.error(s"Invalid response received.", re) - BadGateway - case ce: ConnectException => - log.warn( - message( - s"Unable to search for cover '$coverName'. Unable to connect to cover backend: ${ce.getMessage}" - ), - ce - ) - BadGateway - case t: Throwable => - log.error(message(s"Failure while searching cover '$coverName'."), t) - InternalServerError - } - } - result getOrElse Future.successful(BadRequest) - } - } - - def notFound(message: String) = NotFound(Json.toJson(Errors(Seq(SingleError(message))))) -} diff --git a/musicmeta/app/controllers/LogStreamer.scala b/musicmeta/app/controllers/LogStreamer.scala deleted file mode 100644 index 7b17b0ce..00000000 --- a/musicmeta/app/controllers/LogStreamer.scala +++ /dev/null @@ -1,48 +0,0 @@ -package controllers - -import akka.NotUsed -import akka.stream.scaladsl.{Flow, Sink, Source} -import com.malliina.http.{Errors, SingleError} -import com.malliina.logback.LogbackUtils -import com.malliina.logback.akka.DefaultAkkaAppender -import com.malliina.logstreams.client.LogEvents -import com.malliina.play.auth.UserAuthenticator -import controllers.LogStreamer.log -import play.api.Logger -import play.api.libs.json.{JsValue, Json} -import play.api.mvc.Results.Unauthorized -import play.api.mvc.{Call, WebSocket} - -import scala.concurrent.ExecutionContext -import scala.concurrent.duration.DurationInt - -object LogStreamer { - private val log = Logger(getClass) - - def apply(ec: ExecutionContext): LogStreamer = - new LogStreamer()(ec) -} - -class LogStreamer()(implicit ec: ExecutionContext) { - lazy val jsonEvents: Source[JsValue, NotUsed] = LogbackUtils - .getAppender[DefaultAkkaAppender]("AKKA") - .logEvents - .groupedWithin(5, 100.millis) - .filter(_.nonEmpty) - .map(es => Json.toJson(LogEvents(es))) - val auth = UserAuthenticator.session() - - def openSocket = WebSocket.acceptOrResult[JsValue, JsValue] { rh => - UserAuthenticator.session().authenticate(rh).map { authResult => - authResult.map { ok => - log.info(s"Opening logs socket for '${ok.name}'...") - Flow.fromSinkAndSource(Sink.ignore, jsonEvents) - }.left.map { err => - log.error(s"Unauthorized request '$rh': '$err'.") - Unauthorized(Json.toJson(Errors(Seq(SingleError("Access denied."))))) - } - } - } - - def openSocketCall: Call = routes.MetaOAuth.openSocket -} diff --git a/musicmeta/app/controllers/MetaAssets.scala b/musicmeta/app/controllers/MetaAssets.scala deleted file mode 100644 index b4a9acb7..00000000 --- a/musicmeta/app/controllers/MetaAssets.scala +++ /dev/null @@ -1,7 +0,0 @@ -package controllers - -import controllers.Assets.Asset - -class MetaAssets(builder: AssetsBuilder) { - def versioned(path: String, file: Asset) = builder.versioned(path, file) -} diff --git a/musicmeta/app/controllers/MetaOAuth.scala b/musicmeta/app/controllers/MetaOAuth.scala deleted file mode 100644 index 4bdd83aa..00000000 --- a/musicmeta/app/controllers/MetaOAuth.scala +++ /dev/null @@ -1,54 +0,0 @@ -package controllers - -import com.malliina.concurrent.Execution.cached -import com.malliina.musicmeta.{BuildMeta, MetaHtml, UserFeedback} -import com.malliina.play.ActorExecution -import com.malliina.play.controllers.{AuthBundle, BaseSecurity} -import com.malliina.play.http.AuthedRequest -import com.malliina.play.models.AuthRequest -import play.api.libs.json.Json -import play.api.mvc.Results.{Ok, Redirect} -import play.api.mvc.{ActionBuilder, AnyContent, Request} - -object MetaOAuth { - def apply( - sessionKey: String, - html: MetaHtml, - actions: ActionBuilder[Request, AnyContent], - ctx: ActorExecution - ) = { - val bundle = AuthBundle.oauth( - (r, u) => AuthedRequest(u, r), - routes.MetaOAuthControl.googleStart, - sessionKey - ) - new MetaOAuth(html, actions, bundle, ctx) - } -} - -class MetaOAuth( - html: MetaHtml, - actions: ActionBuilder[Request, AnyContent], - auth: AuthBundle[AuthRequest], - ctx: ActorExecution -) extends BaseSecurity(actions, auth, ctx.materializer) { - - val streamer = LogStreamer(ctx.executionContext) - - def index = authAction(_ => Redirect(routes.MetaOAuth.logs)) - - def health = actions(Ok(Json.toJson(BuildMeta.default))) - - def logs = authAction(_ => Ok(html.logs(None))) - - def openSocket = streamer.openSocket - - def eject = logged { - actions { req => Ok(html.eject(UserFeedback.flashed(req))) } - } - - def logout = authAction { _ => - Redirect(routes.MetaOAuth.eject).withNewSession - .flashing(UserFeedback.success("Logged out.").flash) - } -} diff --git a/musicmeta/app/controllers/MetaOAuthControl.scala b/musicmeta/app/controllers/MetaOAuthControl.scala deleted file mode 100644 index 4cde2fe3..00000000 --- a/musicmeta/app/controllers/MetaOAuthControl.scala +++ /dev/null @@ -1,41 +0,0 @@ -package controllers - -import com.malliina.http.io.HttpClientIO -import com.malliina.oauth.GoogleOAuthCredentials -import com.malliina.play.auth.{AuthHandler, GoogleCodeValidator, OAuthConf} -import com.malliina.values.Email -import com.malliina.web.{AuthConf, AuthError, ClientId, ClientSecret} -import play.api.libs.json.Json -import play.api.mvc.Results.{Redirect, Unauthorized} -import play.api.mvc._ - -class MetaOAuthControl( - val actions: ActionBuilder[Request, AnyContent], - creds: GoogleOAuthCredentials -) { - val http = HttpClientIO() - val handler: AuthHandler = new AuthHandler { - override def onAuthenticated(email: Email, req: RequestHeader): Result = - if (email == Email("malliina123@gmail.com")) - Redirect(routes.MetaOAuth.logs).withSession("username" -> email.email) - else - ejectWith(s"Not authorized: '$email'.") - - override def onUnauthorized(error: AuthError, req: RequestHeader): Result = - Unauthorized(Json.obj("message" -> "Authentication failed.")) - - def ejectWith(message: String) = - Redirect(routes.MetaOAuth.eject).flashing("message" -> message) - } - val authConf = OAuthConf( - routes.MetaOAuthControl.googleCallback, - handler, - AuthConf(ClientId(creds.clientId), ClientSecret(creds.clientSecret)), - http - ) - val validator = GoogleCodeValidator(authConf) - - def googleStart = actions.async { req => validator.start(req).unsafeToFuture() } - - def googleCallback = actions.async { req => validator.validateCallback(req).unsafeToFuture() } -} diff --git a/musicmeta/conf/application.conf b/musicmeta/conf/application.conf deleted file mode 100644 index fd0afcf6..00000000 --- a/musicmeta/conf/application.conf +++ /dev/null @@ -1,11 +0,0 @@ -play { - application.loader = com.malliina.musicmeta.AppLoader - i18n.langs = ["en"] - http { - secret { - key = "changeme" - key = ${?APPLICATION_SECRET} - } - forwarded.trustedProxies = ["0.0.0.0/0", "::/0"] - } -} diff --git a/musicmeta/conf/logback.xml b/musicmeta/conf/logback.xml deleted file mode 100644 index d494b5ff..00000000 --- a/musicmeta/conf/logback.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - yyyy-MM-dd HH:mm:ss - - - - %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n - - - - ${LOGSTREAMS_HOST:-logs.malliina.com} - true - ${LOGSTREAMS_USER:-musicmeta} - ${LOGSTREAMS_PASS} - false - - - - - - - - - - - - - diff --git a/musicmeta/conf/routes b/musicmeta/conf/routes deleted file mode 100644 index 8faa5874..00000000 --- a/musicmeta/conf/routes +++ /dev/null @@ -1,11 +0,0 @@ -GET / controllers.MetaOAuth.index -GET /health controllers.MetaOAuth.health -GET /logs controllers.MetaOAuth.logs -GET /oauth controllers.MetaOAuthControl.googleStart -GET /oauthcb controllers.MetaOAuthControl.googleCallback -GET /eject controllers.MetaOAuth.eject -GET /logout controllers.MetaOAuth.logout -GET /ping controllers.Covers.ping -GET /covers controllers.Covers.cover -GET /ws controllers.MetaOAuth.openSocket -GET /assets/*file controllers.MetaAssets.versioned(path = "/public", file: Asset) diff --git a/musicmeta/frontend/css/custom.less b/musicmeta/frontend/css/custom.less deleted file mode 100644 index fdb16b73..00000000 --- a/musicmeta/frontend/css/custom.less +++ /dev/null @@ -1,38 +0,0 @@ -h1, h2 { - margin-top: 20px; - margin-bottom: 10px; -} - -.navbar { - margin-bottom: 0; -} - -.page-header { - margin: 40px 0 20px; - border-bottom: 1px solid #eee; -} - -.table .table-button { - padding-top: 8px; - padding-bottom: 8px; -} - -.wide-content { - padding: 0 15px; - margin: 0 auto; -} - -.hidden { - display: none; -} - -.compact-group { - margin-bottom: 8px; - display: inline-block; -} - -.verbose { - &.off { - display: none; - } -} diff --git a/musicmeta/frontend/css/footer.less b/musicmeta/frontend/css/footer.less deleted file mode 100644 index 842523e5..00000000 --- a/musicmeta/frontend/css/footer.less +++ /dev/null @@ -1,23 +0,0 @@ -@footer-height: 60px; - -html { - position: relative; - min-height: 100%; -} - -body { - margin-bottom: @footer-height; -} - -.footer { - position: absolute; - bottom: 0; - width: 100%; - height: @footer-height; - line-height: @footer-height; - background-color: #f5f5f5; -} - -code { - font-size: 80%; -} diff --git a/musicmeta/frontend/css/musicmeta.js b/musicmeta/frontend/css/musicmeta.js deleted file mode 100644 index 05b662b6..00000000 --- a/musicmeta/frontend/css/musicmeta.js +++ /dev/null @@ -1 +0,0 @@ -import './musicmeta.less'; diff --git a/musicmeta/frontend/css/musicmeta.less b/musicmeta/frontend/css/musicmeta.less deleted file mode 100644 index ad04b905..00000000 --- a/musicmeta/frontend/css/musicmeta.less +++ /dev/null @@ -1,3 +0,0 @@ -@import 'custom'; -@import 'open-iconic-bootstrap'; -@import 'footer'; diff --git a/musicmeta/frontend/css/open-iconic-bootstrap.less b/musicmeta/frontend/css/open-iconic-bootstrap.less deleted file mode 100644 index 2bea4ce4..00000000 --- a/musicmeta/frontend/css/open-iconic-bootstrap.less +++ /dev/null @@ -1,960 +0,0 @@ -/* Bootstrap */ - -/* Override Bootstrap default variable */ -@icon-font-path: "../fonts/"; - -@font-face { - font-family: 'Icons'; - src: ~"url('@{icon-font-path}open-iconic.eot')"; - src: ~"url('@{icon-font-path}open-iconic.eot?#iconic-sm') format('embedded-opentype')", - ~"url('@{icon-font-path}open-iconic.woff') format('woff')", - ~"url('@{icon-font-path}open-iconic.ttf') format('truetype')", - ~"url('@{icon-font-path}open-iconic.svg#iconic-sm') format('svg')"; - font-weight: normal; - font-style: normal; -} - -// Catchall baseclass -.oi { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Icons'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - &:empty:before { - width: 1em; - text-align: center; - box-sizing: content-box; - } - - &.oi-align-center:before { - text-align: center; - } - - &.oi-align-left:before { - text-align: left; - } - - &.oi-align-right:before { - text-align: right; - } - - - &.oi-flip-horizontal:before { - -webkit-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - transform: scale(-1, 1); - } - - &.oi-flip-vertical:before { - -webkit-transform: scale(1, -1); - -ms-transform: scale(-1, 1); - transform: scale(1, -1); - } - - &.oi-flip-horizontal-vertical:before { - -webkit-transform: scale(-1, -1); - -ms-transform: scale(-1, 1); - transform: scale(-1, -1); - } -} - - - -.oi-account-login:before { - content:"\e000"; -} - -.oi-account-logout:before { - content:"\e001"; -} - -.oi-action-redo:before { - content:"\e002"; -} - -.oi-action-undo:before { - content:"\e003"; -} - -.oi-align-center:before { - content:"\e004"; -} - -.oi-align-left:before { - content:"\e005"; -} - -.oi-align-right:before { - content:"\e006"; -} - -.oi-aperture:before { - content:"\e007"; -} - -.oi-arrow-bottom:before { - content:"\e008"; -} - -.oi-arrow-circle-bottom:before { - content:"\e009"; -} - -.oi-arrow-circle-left:before { - content:"\e00a"; -} - -.oi-arrow-circle-right:before { - content:"\e00b"; -} - -.oi-arrow-circle-top:before { - content:"\e00c"; -} - -.oi-arrow-left:before { - content:"\e00d"; -} - -.oi-arrow-right:before { - content:"\e00e"; -} - -.oi-arrow-thick-bottom:before { - content:"\e00f"; -} - -.oi-arrow-thick-left:before { - content:"\e010"; -} - -.oi-arrow-thick-right:before { - content:"\e011"; -} - -.oi-arrow-thick-top:before { - content:"\e012"; -} - -.oi-arrow-top:before { - content:"\e013"; -} - -.oi-audio-spectrum:before { - content:"\e014"; -} - -.oi-audio:before { - content:"\e015"; -} - -.oi-badge:before { - content:"\e016"; -} - -.oi-ban:before { - content:"\e017"; -} - -.oi-bar-chart:before { - content:"\e018"; -} - -.oi-basket:before { - content:"\e019"; -} - -.oi-battery-empty:before { - content:"\e01a"; -} - -.oi-battery-full:before { - content:"\e01b"; -} - -.oi-beaker:before { - content:"\e01c"; -} - -.oi-bell:before { - content:"\e01d"; -} - -.oi-bluetooth:before { - content:"\e01e"; -} - -.oi-bold:before { - content:"\e01f"; -} - -.oi-bolt:before { - content:"\e020"; -} - -.oi-book:before { - content:"\e021"; -} - -.oi-bookmark:before { - content:"\e022"; -} - -.oi-box:before { - content:"\e023"; -} - -.oi-briefcase:before { - content:"\e024"; -} - -.oi-british-pound:before { - content:"\e025"; -} - -.oi-browser:before { - content:"\e026"; -} - -.oi-brush:before { - content:"\e027"; -} - -.oi-bug:before { - content:"\e028"; -} - -.oi-bullhorn:before { - content:"\e029"; -} - -.oi-calculator:before { - content:"\e02a"; -} - -.oi-calendar:before { - content:"\e02b"; -} - -.oi-camera-slr:before { - content:"\e02c"; -} - -.oi-caret-bottom:before { - content:"\e02d"; -} - -.oi-caret-left:before { - content:"\e02e"; -} - -.oi-caret-right:before { - content:"\e02f"; -} - -.oi-caret-top:before { - content:"\e030"; -} - -.oi-cart:before { - content:"\e031"; -} - -.oi-chat:before { - content:"\e032"; -} - -.oi-check:before { - content:"\e033"; -} - -.oi-chevron-bottom:before { - content:"\e034"; -} - -.oi-chevron-left:before { - content:"\e035"; -} - -.oi-chevron-right:before { - content:"\e036"; -} - -.oi-chevron-top:before { - content:"\e037"; -} - -.oi-circle-check:before { - content:"\e038"; -} - -.oi-circle-x:before { - content:"\e039"; -} - -.oi-clipboard:before { - content:"\e03a"; -} - -.oi-clock:before { - content:"\e03b"; -} - -.oi-cloud-download:before { - content:"\e03c"; -} - -.oi-cloud-upload:before { - content:"\e03d"; -} - -.oi-cloud:before { - content:"\e03e"; -} - -.oi-cloudy:before { - content:"\e03f"; -} - -.oi-code:before { - content:"\e040"; -} - -.oi-cog:before { - content:"\e041"; -} - -.oi-collapse-down:before { - content:"\e042"; -} - -.oi-collapse-left:before { - content:"\e043"; -} - -.oi-collapse-right:before { - content:"\e044"; -} - -.oi-collapse-up:before { - content:"\e045"; -} - -.oi-command:before { - content:"\e046"; -} - -.oi-comment-square:before { - content:"\e047"; -} - -.oi-compass:before { - content:"\e048"; -} - -.oi-contrast:before { - content:"\e049"; -} - -.oi-copywriting:before { - content:"\e04a"; -} - -.oi-credit-card:before { - content:"\e04b"; -} - -.oi-crop:before { - content:"\e04c"; -} - -.oi-dashboard:before { - content:"\e04d"; -} - -.oi-data-transfer-download:before { - content:"\e04e"; -} - -.oi-data-transfer-upload:before { - content:"\e04f"; -} - -.oi-delete:before { - content:"\e050"; -} - -.oi-dial:before { - content:"\e051"; -} - -.oi-document:before { - content:"\e052"; -} - -.oi-dollar:before { - content:"\e053"; -} - -.oi-double-quote-sans-left:before { - content:"\e054"; -} - -.oi-double-quote-sans-right:before { - content:"\e055"; -} - -.oi-double-quote-serif-left:before { - content:"\e056"; -} - -.oi-double-quote-serif-right:before { - content:"\e057"; -} - -.oi-droplet:before { - content:"\e058"; -} - -.oi-eject:before { - content:"\e059"; -} - -.oi-elevator:before { - content:"\e05a"; -} - -.oi-ellipses:before { - content:"\e05b"; -} - -.oi-envelope-closed:before { - content:"\e05c"; -} - -.oi-envelope-open:before { - content:"\e05d"; -} - -.oi-euro:before { - content:"\e05e"; -} - -.oi-excerpt:before { - content:"\e05f"; -} - -.oi-expand-down:before { - content:"\e060"; -} - -.oi-expand-left:before { - content:"\e061"; -} - -.oi-expand-right:before { - content:"\e062"; -} - -.oi-expand-up:before { - content:"\e063"; -} - -.oi-external-link:before { - content:"\e064"; -} - -.oi-eye:before { - content:"\e065"; -} - -.oi-eyedropper:before { - content:"\e066"; -} - -.oi-file:before { - content:"\e067"; -} - -.oi-fire:before { - content:"\e068"; -} - -.oi-flag:before { - content:"\e069"; -} - -.oi-flash:before { - content:"\e06a"; -} - -.oi-folder:before { - content:"\e06b"; -} - -.oi-fork:before { - content:"\e06c"; -} - -.oi-fullscreen-enter:before { - content:"\e06d"; -} - -.oi-fullscreen-exit:before { - content:"\e06e"; -} - -.oi-globe:before { - content:"\e06f"; -} - -.oi-graph:before { - content:"\e070"; -} - -.oi-grid-four-up:before { - content:"\e071"; -} - -.oi-grid-three-up:before { - content:"\e072"; -} - -.oi-grid-two-up:before { - content:"\e073"; -} - -.oi-hard-drive:before { - content:"\e074"; -} - -.oi-header:before { - content:"\e075"; -} - -.oi-headphones:before { - content:"\e076"; -} - -.oi-heart:before { - content:"\e077"; -} - -.oi-home:before { - content:"\e078"; -} - -.oi-image:before { - content:"\e079"; -} - -.oi-inbox:before { - content:"\e07a"; -} - -.oi-infinity:before { - content:"\e07b"; -} - -.oi-info:before { - content:"\e07c"; -} - -.oi-italic:before { - content:"\e07d"; -} - -.oi-justify-center:before { - content:"\e07e"; -} - -.oi-justify-left:before { - content:"\e07f"; -} - -.oi-justify-right:before { - content:"\e080"; -} - -.oi-key:before { - content:"\e081"; -} - -.oi-laptop:before { - content:"\e082"; -} - -.oi-layers:before { - content:"\e083"; -} - -.oi-lightbulb:before { - content:"\e084"; -} - -.oi-link-broken:before { - content:"\e085"; -} - -.oi-link-intact:before { - content:"\e086"; -} - -.oi-list-rich:before { - content:"\e087"; -} - -.oi-list:before { - content:"\e088"; -} - -.oi-location:before { - content:"\e089"; -} - -.oi-lock-locked:before { - content:"\e08a"; -} - -.oi-lock-unlocked:before { - content:"\e08b"; -} - -.oi-loop-circular:before { - content:"\e08c"; -} - -.oi-loop-square:before { - content:"\e08d"; -} - -.oi-loop:before { - content:"\e08e"; -} - -.oi-magnifying-glass:before { - content:"\e08f"; -} - -.oi-map-marker:before { - content:"\e090"; -} - -.oi-map:before { - content:"\e091"; -} - -.oi-media-pause:before { - content:"\e092"; -} - -.oi-media-play:before { - content:"\e093"; -} - -.oi-media-record:before { - content:"\e094"; -} - -.oi-media-skip-backward:before { - content:"\e095"; -} - -.oi-media-skip-forward:before { - content:"\e096"; -} - -.oi-media-step-backward:before { - content:"\e097"; -} - -.oi-media-step-forward:before { - content:"\e098"; -} - -.oi-media-stop:before { - content:"\e099"; -} - -.oi-medical-cross:before { - content:"\e09a"; -} - -.oi-menu:before { - content:"\e09b"; -} - -.oi-microphone:before { - content:"\e09c"; -} - -.oi-minus:before { - content:"\e09d"; -} - -.oi-monitor:before { - content:"\e09e"; -} - -.oi-moon:before { - content:"\e09f"; -} - -.oi-move:before { - content:"\e0a0"; -} - -.oi-musical-note:before { - content:"\e0a1"; -} - -.oi-paperclip:before { - content:"\e0a2"; -} - -.oi-pencil:before { - content:"\e0a3"; -} - -.oi-people:before { - content:"\e0a4"; -} - -.oi-person:before { - content:"\e0a5"; -} - -.oi-phone:before { - content:"\e0a6"; -} - -.oi-pie-chart:before { - content:"\e0a7"; -} - -.oi-pin:before { - content:"\e0a8"; -} - -.oi-play-circle:before { - content:"\e0a9"; -} - -.oi-plus:before { - content:"\e0aa"; -} - -.oi-power-standby:before { - content:"\e0ab"; -} - -.oi-print:before { - content:"\e0ac"; -} - -.oi-project:before { - content:"\e0ad"; -} - -.oi-pulse:before { - content:"\e0ae"; -} - -.oi-puzzle-piece:before { - content:"\e0af"; -} - -.oi-question-mark:before { - content:"\e0b0"; -} - -.oi-rain:before { - content:"\e0b1"; -} - -.oi-random:before { - content:"\e0b2"; -} - -.oi-reload:before { - content:"\e0b3"; -} - -.oi-resize-both:before { - content:"\e0b4"; -} - -.oi-resize-height:before { - content:"\e0b5"; -} - -.oi-resize-width:before { - content:"\e0b6"; -} - -.oi-rss-alt:before { - content:"\e0b7"; -} - -.oi-rss:before { - content:"\e0b8"; -} - -.oi-script:before { - content:"\e0b9"; -} - -.oi-share-boxed:before { - content:"\e0ba"; -} - -.oi-share:before { - content:"\e0bb"; -} - -.oi-shield:before { - content:"\e0bc"; -} - -.oi-signal:before { - content:"\e0bd"; -} - -.oi-signpost:before { - content:"\e0be"; -} - -.oi-sort-ascending:before { - content:"\e0bf"; -} - -.oi-sort-descending:before { - content:"\e0c0"; -} - -.oi-spreadsheet:before { - content:"\e0c1"; -} - -.oi-star:before { - content:"\e0c2"; -} - -.oi-sun:before { - content:"\e0c3"; -} - -.oi-tablet:before { - content:"\e0c4"; -} - -.oi-tag:before { - content:"\e0c5"; -} - -.oi-tags:before { - content:"\e0c6"; -} - -.oi-target:before { - content:"\e0c7"; -} - -.oi-task:before { - content:"\e0c8"; -} - -.oi-terminal:before { - content:"\e0c9"; -} - -.oi-text:before { - content:"\e0ca"; -} - -.oi-thumb-down:before { - content:"\e0cb"; -} - -.oi-thumb-up:before { - content:"\e0cc"; -} - -.oi-timer:before { - content:"\e0cd"; -} - -.oi-transfer:before { - content:"\e0ce"; -} - -.oi-trash:before { - content:"\e0cf"; -} - -.oi-underline:before { - content:"\e0d0"; -} - -.oi-vertical-align-bottom:before { - content:"\e0d1"; -} - -.oi-vertical-align-center:before { - content:"\e0d2"; -} - -.oi-vertical-align-top:before { - content:"\e0d3"; -} - -.oi-video:before { - content:"\e0d4"; -} - -.oi-volume-high:before { - content:"\e0d5"; -} - -.oi-volume-low:before { - content:"\e0d6"; -} - -.oi-volume-off:before { - content:"\e0d7"; -} - -.oi-warning:before { - content:"\e0d8"; -} - -.oi-wifi:before { - content:"\e0d9"; -} - -.oi-wrench:before { - content:"\e0da"; -} - -.oi-x:before { - content:"\e0db"; -} - -.oi-yen:before { - content:"\e0dc"; -} - -.oi-zoom-in:before { - content:"\e0dd"; -} - -.oi-zoom-out:before { - content:"\e0de"; -} - diff --git a/musicmeta/frontend/fonts/open-iconic.eot b/musicmeta/frontend/fonts/open-iconic.eot deleted file mode 100755 index f98177dbf711863eff7c90f84d5d419d02d99ba8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28196 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 f%X4~$E_2;^J#ifP;CI~=<%5iE_!YyhznS - - - - -Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 - By P.J. Onori -Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/musicmeta/frontend/fonts/open-iconic.ttf b/musicmeta/frontend/fonts/open-iconic.ttf deleted file mode 100755 index fab604866cd5e55ef4525ea22e420c411f510b01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28028 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 diff --git a/musicmeta/frontend/fonts/open-iconic.woff b/musicmeta/frontend/fonts/open-iconic.woff deleted file mode 100755 index f9309988aeab3868040d3b322658902098eba27f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14984 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 diff --git a/musicmeta/frontend/postcss.config.js b/musicmeta/frontend/postcss.config.js deleted file mode 100644 index f5aed9b9..00000000 --- a/musicmeta/frontend/postcss.config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - plugins: { - 'postcss-import': {}, - 'postcss-preset-env': {}, - 'cssnano': {}, - 'autoprefixer': {} - } -}; diff --git a/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/MetaFrontend.scala b/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/MetaFrontend.scala deleted file mode 100644 index 3e3359d3..00000000 --- a/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/MetaFrontend.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.malliina.musicmeta.js - -import org.scalajs.jquery.JQueryStatic - -import scala.scalajs.js -import scala.scalajs.js.annotation.JSImport - -object MetaFrontend { - private val jq = MyJQuery - private val p = Popper - private val b = Bootstrap - - def main(args: Array[String]): Unit = { - new MetaSocket - } -} - -@js.native -@JSImport("jquery", JSImport.Namespace) -object MyJQuery extends JQueryStatic - -@js.native -@JSImport("popper.js", JSImport.Namespace) -object Popper extends js.Object - -@js.native -@JSImport("bootstrap", JSImport.Namespace) -object Bootstrap extends js.Object diff --git a/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/MetaSocket.scala b/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/MetaSocket.scala deleted file mode 100644 index 9422fa0b..00000000 --- a/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/MetaSocket.scala +++ /dev/null @@ -1,101 +0,0 @@ -package com.malliina.musicmeta.js - -import com.malliina.http.FullUrl -import org.scalajs.dom -import org.scalajs.dom.{CloseEvent, WebSocket, document} -import org.scalajs.dom.html.TableSection -import org.scalajs.dom.raw.{ErrorEvent, Event, HTMLElement, MessageEvent} -import play.api.libs.json.{JsError, JsValue, Json} -import scalatags.JsDom.all._ - -import scala.util.Try - -class MetaSocket { - val OptionVerboseId = "option-verbose" - val OptionCompactId = "option-compact" - val tableContent = elem[TableSection]("log-table-body") - val socket = openSocket("/ws?f=json") - - var isVerbose: Boolean = false - - installClick("label-verbose")(_ => updateVerbose(true)) - installClick("label-compact")(_ => updateVerbose(false)) - - def updateVerbose(newVerbose: Boolean): Unit = { - isVerbose = newVerbose - document.getElementsByClassName("verbose").foreach { e => - val classes = e.asInstanceOf[HTMLElement].classList - if (newVerbose) classes.remove("off") else classes.add("off") - } - } - - def installClick(on: String)(onClick: Event => Unit): Unit = - elem[HTMLElement](on).addEventListener("click", onClick) - - def onMessage(msg: MessageEvent): Unit = { - Try(Json.parse(msg.data.toString)).map { json => - val isPing = (json \ "event").validate[String].filter(_ == "ping").isSuccess - if (!isPing) { - handlePayload(json) - } - }.recover { case e => onJsonFailure(e) } - } - - def handlePayload(value: JsValue): Unit = { - value.validate[LogEvents].fold(err => onJsonFailure(JsError(err)), prependAll) - } - - def onJsonFailure(error: Any) = { - println(error) - } - - def prependAll(events: LogEvents): Unit = events.events foreach prepend - - def prepend(event: LogEvent) = - Option(tableContent.firstChild).map { first => - tableContent.insertBefore(row(event).render, first) - }.getOrElse { - tableContent.appendChild(row(event).render) - } - - def row(event: LogEvent) = { - val opening = if (event.isError) tr(`class` := "danger") else tr - val verboseClass = names("verbose", if (isVerbose) "" else "off") - opening( - td(event.timeFormatted), - td(event.message), - td(`class` := verboseClass)(event.loggerName), - td(event.level) - ) - } - - def onConnected(e: Event): Unit = updateStatus("Connected.") - - def onClosed(e: CloseEvent): Unit = updateStatus("Closed.") - - def onError(e: Event): Unit = updateStatus("Error.") - - def updateStatus(status: String): Unit = { - document.getElementById("status").innerHTML = status - } - - def openSocket(pathAndQuery: String): WebSocket = { - val url = wsBaseUrl.append(pathAndQuery) - val socket = new dom.WebSocket(url.url) - socket.onopen = (e: Event) => onConnected(e) - socket.onmessage = (e: MessageEvent) => onMessage(e) - socket.onclose = (e: CloseEvent) => onClosed(e) - socket.onerror = (e: Event) => onError(e) - socket - } - - def wsBaseUrl: FullUrl = { - val location = dom.window.location - val wsProto = if (location.protocol == "http:") "ws" else "wss" - FullUrl(wsProto, location.host, "") - } - - def elem[T](id: String) = document.getElementById(id).asInstanceOf[T] - - def names(ns: String*): String = ns.map(_.trim).filter(_.nonEmpty).mkString(" ") -} diff --git a/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/models.scala b/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/models.scala deleted file mode 100644 index 5df37725..00000000 --- a/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/models.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.malliina.musicmeta.js - -import play.api.libs.json.{Json, OFormat} - -case class LogEvent(timestamp: Long, - timeFormatted: String, - message: String, - loggerName: String, - threadName: String, - level: String) { - def isError = level == "ERROR" -} - -object LogEvent { - implicit val json: OFormat[LogEvent] = Json.format[LogEvent] -} - -case class LogEvents(events: Seq[LogEvent]) - -object LogEvents { - implicit val json: OFormat[LogEvents] = Json.format[LogEvents] -} diff --git a/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/package.scala b/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/package.scala deleted file mode 100644 index 81ecd0e8..00000000 --- a/musicmeta/frontend/src/main/scala/com/malliina/musicmeta/js/package.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.malliina.musicmeta - -import org.scalajs.dom.{DOMList, Node} - -package object js { - - implicit class NodeListSeq[T <: Node](nodes: DOMList[T]) extends IndexedSeq[T] { - override def foreach[U](f: T => U): Unit = { - for (i <- 0 until nodes.length) { - f(nodes(i)) - } - } - - override def length: Int = nodes.length - - override def apply(idx: Int): T = nodes(idx) - } - -} diff --git a/musicmeta/frontend/webpack.base.config.js b/musicmeta/frontend/webpack.base.config.js deleted file mode 100644 index 0f60e8ba..00000000 --- a/musicmeta/frontend/webpack.base.config.js +++ /dev/null @@ -1,34 +0,0 @@ -const ScalaJS = require('./scalajs.webpack.config'); -const Merge = require('webpack-merge'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const path = require('path'); -const rootDir = path.resolve(__dirname, '../../../..'); -const cssDir = path.resolve(rootDir, 'css'); - -const WebApp = Merge(ScalaJS, { - entry: { - styles: [path.resolve(cssDir, './musicmeta.js')] - }, - module: { - rules: [ - { - test: /\.(png|woff|woff2|eot|ttf|svg)$/, - type: 'asset/inline' - }, - { - test: /\.less$/, - use: [ - MiniCssExtractPlugin.loader, - { loader: 'css-loader', options: { importLoaders: 1 } }, - 'postcss-loader', - 'less-loader' - ] - } - ] - }, - plugins: [ - new MiniCssExtractPlugin({filename: '[name].css'}) - ] -}); - -module.exports = WebApp; diff --git a/musicmeta/frontend/webpack.dev.config.js b/musicmeta/frontend/webpack.dev.config.js deleted file mode 100644 index fcb53d7c..00000000 --- a/musicmeta/frontend/webpack.dev.config.js +++ /dev/null @@ -1,6 +0,0 @@ -const BaseWebpack = require('./webpack.base.config'); -const Merge = require('webpack-merge'); - -module.exports = Merge(BaseWebpack, { - mode: 'development' -}); diff --git a/musicmeta/frontend/webpack.prod.config.js b/musicmeta/frontend/webpack.prod.config.js deleted file mode 100644 index 5085bdb7..00000000 --- a/musicmeta/frontend/webpack.prod.config.js +++ /dev/null @@ -1,6 +0,0 @@ -const BaseWebpack = require('./webpack.base.config'); -const Merge = require('webpack-merge'); - -module.exports = Merge(BaseWebpack, { - mode: 'production' -}); diff --git a/musicmeta/src/pkg/musicmeta.conf b/musicmeta/src/pkg/musicmeta.conf deleted file mode 100644 index 45a21c97..00000000 --- a/musicmeta/src/pkg/musicmeta.conf +++ /dev/null @@ -1 +0,0 @@ -cover.dir=/opt/musicmeta/covers/ \ No newline at end of file diff --git a/musicmeta/src/pkg/unix/changelog b/musicmeta/src/pkg/unix/changelog deleted file mode 100644 index a0caabaa..00000000 --- a/musicmeta/src/pkg/unix/changelog +++ /dev/null @@ -1 +0,0 @@ -Everything changes \ No newline at end of file diff --git a/musicmeta/src/pkg/unix/control/postinstall.sh b/musicmeta/src/pkg/unix/control/postinstall.sh deleted file mode 100644 index a356f414..00000000 --- a/musicmeta/src/pkg/unix/control/postinstall.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/sh -set -e -echo -n "Executing postinstall..." -APP_NAME=musicmeta -if [ -f /etc/default/${APP_NAME} ] ; then - . /etc/default/${APP_NAME} -fi - -# Creates user -user=`id -nu ${APP_USER} 2>/dev/null || echo ""` -if [ "${user}" = "${APP_USER}" ]; then - echo -n "User already exists..." -else - echo -n "Creating user ${APP_USER}..." - useradd -s /bin/false ${APP_USER} - if [ ! $? ]; then - echo -n "Unable to create user" - exit 666 - fi -fi - -# Sets permissions -chown -R ${APP_USER}:${APP_USER} ${APP_HOME} - -# Installs as service -# Use update-rc.d for debian/ubuntu else chkconfig -if [ -x /usr/sbin/update-rc.d ]; then - echo -n "Adding as service with update-rc.d..." - update-rc.d ${APP_NAME} defaults && serviceOK=true -else - echo -n "Initializing service with chkconfig..." - chkconfig --add ${APP_NAME} && chkconfig ${APP_NAME} on && chkconfig --list ${APP_NAME} && serviceOK=true -fi -if [ ! ${serviceOK} ]; then - echo -n "Error adding service" - exit 1 -fi - -echo "Installation complete. You can now use 'service ${APP_NAME} start/stop/restart/status'." - diff --git a/musicmeta/src/pkg/unix/control/postuninstall.sh b/musicmeta/src/pkg/unix/control/postuninstall.sh deleted file mode 100644 index a61e0bcc..00000000 --- a/musicmeta/src/pkg/unix/control/postuninstall.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -set -e -echo -n "Executing postuninstall..." -# the user is removed already in preuninstall -echo "Postuninstall done." diff --git a/musicmeta/src/pkg/unix/control/preinstall.sh b/musicmeta/src/pkg/unix/control/preinstall.sh deleted file mode 100644 index 2abec0bf..00000000 --- a/musicmeta/src/pkg/unix/control/preinstall.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -e -echo -n "Executing preinstall..." -echo "Preinstall done." diff --git a/musicmeta/src/pkg/unix/control/preuninstall.sh b/musicmeta/src/pkg/unix/control/preuninstall.sh deleted file mode 100644 index 59d5c8b5..00000000 --- a/musicmeta/src/pkg/unix/control/preuninstall.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -set -e -echo -n "Executing preuninstall..." -APP_NAME=musicmeta -if [ -f /etc/default/${APP_NAME} ] ; then - . /etc/default/${APP_NAME} -fi -echo -n "Stopping ${APP_NAME}..." -stopreturn=`service ${APP_NAME} stop 2>/dev/null` -# Deletes user, if it exists -# echo is a hack so the exit value will be 0 -user=`id -nu ${APP_USER} 2>/dev/null || echo ""` -if [ "${user}" = "${APP_USER}" ]; then - echo -n "Deleting user ${APP_USER}..." - userdel ${APP_USER} -fi -echo "Preuninstall done." - diff --git a/musicmeta/src/pkg/unix/copyright b/musicmeta/src/pkg/unix/copyright deleted file mode 100644 index 41dabc84..00000000 --- a/musicmeta/src/pkg/unix/copyright +++ /dev/null @@ -1 +0,0 @@ -Copyright Michael Skogberg 2014 \ No newline at end of file diff --git a/musicmeta/src/pkg/unix/lib/.empty.txt b/musicmeta/src/pkg/unix/lib/.empty.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/musicmeta/src/pkg/unix/logs/.empty.txt b/musicmeta/src/pkg/unix/logs/.empty.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/musicmeta/src/pkg/unix/musicmeta.defaults b/musicmeta/src/pkg/unix/musicmeta.defaults deleted file mode 100644 index 24a22650..00000000 --- a/musicmeta/src/pkg/unix/musicmeta.defaults +++ /dev/null @@ -1,7 +0,0 @@ -APP_NAME=musicmeta -APP_HOME=/opt/musicmeta -APP_USER=musicmeta -JAVA_OPTS="-Dmusicmeta.home=/opt/musicmeta -Dlogger.resource=prod-logger.xml" -JAVA_CMD=/usr/bin/java -PID_FILE=/var/run/musicmeta.pid -MAIN_CLASS=com.mle.musicmeta.Starter diff --git a/musicmeta/src/pkg/unix/musicmeta.sh b/musicmeta/src/pkg/unix/musicmeta.sh deleted file mode 100644 index 9c2e5c41..00000000 --- a/musicmeta/src/pkg/unix/musicmeta.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/sh -# -# chkconfig: 345 80 20 -# description: App -# processname: musicmeta -# pidfile: /var/run/musicmeta.pid -# -### BEGIN INIT INFO -# Provides: musicmeta -# Required-Start: $remote_fs $syslog $network -# Required-Stop: $remote_fs $syslog $network -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start musicmeta at boot time -# Description: Manages the services needed to run musicmeta -### END INIT INFO -# -# Startup script for musicmeta under *nix systems (it works under NT/cygwin too). -# Adapted from artifactory's start/stop script -# Should be LSB compliant and therefore usable with pacemaker for HA configurations - -usage() { - echo "Usage: $0 {start|stop|restart|force-reload|status}" - exit 1 -} -pid_exists(){ - if [ -f ${PID_FILE} ]; then - return 0 - else - return 1 - fi -} -is_running(){ - if pid_exists; then - if [ "$(ps -p `cat ${PID_FILE}` | wc -l)" -gt 1 ]; then - return 0 - else - # not running, but PID file exists - echo "The PID file exists but the app is not running. Removing old pid file." - rm ${PID_FILE} - return 1 - fi - else - return 1 - fi -} - -# Script starts here -APP_NAME=musicmeta -if [ -f /etc/default/${APP_NAME} ] ; then - . /etc/default/${APP_NAME} -fi - -case "$1" in - start) - if is_running; then - echo "Already running" - # LSB says: return 0 when starting an already started service - exit 0 - fi - COMMAND="exec ${JAVA_CMD} ${JAVA_OPTS} -cp ${APP_HOME}/lib/*:${APP_HOME}/${APP_NAME}.jar ${MAIN_CLASS} >> ${APP_HOME}/logs/console.out 2>&1" - if [ -z "${APP_USER}" ]; then - nohup sh -c "${COMMAND}" >/dev/null 2>&1 & - else - nohup su - ${APP_USER} --shell=/bin/sh -c "${COMMAND}" >/dev/null 2>&1 & - fi - echo $! > ${PID_FILE} - sleep 1 - if is_running; then - echo "Started" - else - echo "Startup failed" - exit 1 - fi - ;; - stop) - if pid_exists; then - # TODO: implement graceful shutdown e.g. with remote akka actors - PID=`cat ${PID_FILE} 2>/dev/null` - kill $PID 2>/dev/null - rm -f ${PID_FILE} - # Wait for the service to die; remove if RMI in use - sleep 2 - echo "Stopped" - else - echo "Unable to find PID file; already stopped?" - fi - ;; - restart) - $0 stop $* - $0 start $* - ;; - force-reload) - $0 restart - ;; - status) - if is_running; then - echo "Running" - else - echo "Not running" - # LSB says exit status 3 is "program is not running" - # http://refspecs.linux-foundation.org/LSB_3.2.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html - exit 3 - fi - ;; - *) - usage - ;; -esac -exit 0 - - - diff --git a/musicmeta/test/tests/APITests.scala b/musicmeta/test/tests/APITests.scala deleted file mode 100644 index bf8dea90..00000000 --- a/musicmeta/test/tests/APITests.scala +++ /dev/null @@ -1,74 +0,0 @@ -package tests - -import akka.actor.ActorSystem -import akka.stream.Materializer -import com.malliina.http.DiscoClient.DiscoGsCredentials -import com.malliina.musicmeta.MetaHtml -import com.malliina.oauth.GoogleOAuthCredentials -import com.malliina.play.ActorExecution -import controllers.{Covers, MetaOAuth, MetaOAuthControl} -import play.api.{Mode, http} -import play.api.mvc._ -import play.api.test.FakeRequest -import play.api.test.Helpers._ - -import scala.concurrent.duration.{DurationInt, FiniteDuration} -import scala.concurrent.{Await, Future} - -object APITests { - val fakeCreds = DiscoGsCredentials("token") - val fakeGoogle = GoogleOAuthCredentials("client", "secret", "scope") -} - -class APITests extends munit.FunSuite { - implicit val timeout: FiniteDuration = 20.seconds - implicit val actorSystem: ActorSystem = ActorSystem("test") - val mat = Materializer.matFromSystem(actorSystem) - val oauthControl = - new MetaOAuthControl(stubControllerComponents().actionBuilder, APITests.fakeGoogle) - val exec = ActorExecution(actorSystem, mat) - val oauth = MetaOAuth( - "username", - MetaHtml("musicmeta-frontend", Mode.Test), - stubControllerComponents().actionBuilder, - exec - ) - val covers = new Covers(oauth, APITests.fakeCreds, stubControllerComponents()) - - test("respond to ping") { - verifyActionResponse(covers.ping, OK) - } - - test("proper cover search".ignore) { - verifyActionResponse( - covers.cover, - OK, - FakeRequest(GET, "/covers?artist=iron%20maiden&album=powerslave") - ) - } - - test("nonexistent cover return 404".ignore) { - verifyActionResponse( - covers.cover, - NOT_FOUND, - FakeRequest(GET, "/covers?artist=zyz&album=abcde") - ) - } - - test("invalid request returns HTTP 400 BAD REQUEST") { - verifyActionResponse(covers.cover, http.Status.BAD_REQUEST) - } - - private def verifyActionResponse( - action: EssentialAction, - expectedStatus: Int, - req: FakeRequest[AnyContentAsEmpty.type] = FakeRequest() - ): Unit = { - verifyResponse(action(req).run(), expectedStatus) - } - - private def verifyResponse(result: Future[Result], expectedStatus: Int = http.Status.OK): Unit = { - val statusCode = Await.result(result, timeout).header.status - assert(statusCode == expectedStatus) - } -} diff --git a/musicmeta/test/tests/AppTests.scala b/musicmeta/test/tests/AppTests.scala deleted file mode 100644 index 0fe9d5d6..00000000 --- a/musicmeta/test/tests/AppTests.scala +++ /dev/null @@ -1,26 +0,0 @@ -package tests - -import com.malliina.musicmeta.AppComponents -import play.api.test.FakeRequest -import play.api.test.Helpers._ - -class AppTests - extends AppSuite(new AppComponents(_, _ => APITests.fakeCreds, _ => APITests.fakeGoogle)) { - test("router.ping") { - val result = getRequest("/ping") - assert(status(result) == 200) - } - - test("request to nonexistent URL returns 404") { - val result = getRequest("/ping2") - assert(status(result) == 404) - } - - test("router.badrequest") { - val result = getRequest("/covers?artist=abba&album=") - assert(status(result) == 400) - } - - private def getRequest(path: String) = - route(testApp().application, FakeRequest(GET, path)).get -} diff --git a/musicmeta/test/tests/DiscoGsTests.scala b/musicmeta/test/tests/DiscoGsTests.scala deleted file mode 100644 index dfcdbbad..00000000 --- a/musicmeta/test/tests/DiscoGsTests.scala +++ /dev/null @@ -1,35 +0,0 @@ -package tests - -import java.io.Closeable -import com.malliina.concurrent.Execution.cached -import com.malliina.http.DiscoClient -import com.malliina.http.DiscoClient.DiscoGsCredentials -import com.malliina.oauth.DiscoGsOAuthCredentials -import controllers.Covers - -import scala.concurrent.Await -import scala.concurrent.duration.DurationInt - -class DiscoGsTests extends munit.FunSuite { - val uri = "http://api.discogs.com/image/R-5245462-1388609959-3809.jpeg" - - test("download cover".ignore) { - val creds = DiscoGsCredentials("token") - using(new DiscoClient(creds, Covers.tempDir)) { client => - val result = client - .downloadCover("Iron Maiden", "Powerslave") - .map(p => s"Downloaded to $p") - .recover { case t => s"Failure: $t" } - val r = Await.result(result, 20.seconds) - - assert(r startsWith "Downloaded") - } - } - - def using[T <: Closeable, U](resource: T)(op: T => U): U = - try { - op(resource) - } finally { - resource.close() - } -} diff --git a/musicmeta/test/tests/LogStreamTests.scala b/musicmeta/test/tests/LogStreamTests.scala deleted file mode 100644 index 4f8d0ad9..00000000 --- a/musicmeta/test/tests/LogStreamTests.scala +++ /dev/null @@ -1,14 +0,0 @@ -package tests - -import java.net.URL - -import scala.concurrent.duration.DurationInt -import scala.concurrent.{Await, Future} - -class LogStreamTests extends munit.FunSuite { - test("conn".ignore) { - new URL("https://letsencrypt.org/").openConnection.connect() - } - - def await[T](f: Future[T]) = Await.result(f, 10.seconds) -} diff --git a/musicmeta/test/tests/OtherTests.scala b/musicmeta/test/tests/OtherTests.scala deleted file mode 100644 index 79d0242f..00000000 --- a/musicmeta/test/tests/OtherTests.scala +++ /dev/null @@ -1,11 +0,0 @@ -package tests - -import java.nio.file.Paths - -class OtherTests extends munit.FunSuite { - test("read a path") { - val p = Paths.get("é") - val f = p.toFile - assert(p.toAbsolutePath.toString == f.getAbsolutePath) - } -} diff --git a/project/build.properties b/project/build.properties index 27430827..e8a1e246 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.6 +sbt.version=1.9.7 diff --git a/project/plugins.sbt b/project/plugins.sbt index 5c3eebfb..b53fb2c8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,15 +1,15 @@ scalaVersion := "2.12.18" scalacOptions ++= Seq("-unchecked", "-deprecation", "-language:implicitConversions") -val utilsVersion = "1.6.19" +val utilsVersion = "1.6.29" Seq( "com.typesafe.play" % "sbt-plugin" % "2.8.20", "com.malliina" % "sbt-utils-maven" % utilsVersion, "com.malliina" % "sbt-nodejs" % utilsVersion, - "com.malliina" % "sbt-filetree" % "0.4.1", + "com.malliina" % "sbt-filetree" % utilsVersion, "com.malliina" % "sbt-packager" % "2.10.1", - "org.scala-js" % "sbt-scalajs" % "1.13.2", + "org.scala-js" % "sbt-scalajs" % "1.14.0", "org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2", "com.vmunier" % "sbt-web-scalajs" % "1.2.0", "com.typesafe.sbt" % "sbt-digest" % "1.1.4",