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 f98177db..00000000 Binary files a/musicmeta/frontend/fonts/open-iconic.eot and /dev/null differ diff --git a/musicmeta/frontend/fonts/open-iconic.otf b/musicmeta/frontend/fonts/open-iconic.otf deleted file mode 100755 index f6bd6846..00000000 Binary files a/musicmeta/frontend/fonts/open-iconic.otf and /dev/null differ diff --git a/musicmeta/frontend/fonts/open-iconic.svg b/musicmeta/frontend/fonts/open-iconic.svg deleted file mode 100755 index 32b2c4e9..00000000 --- a/musicmeta/frontend/fonts/open-iconic.svg +++ /dev/null @@ -1,543 +0,0 @@ - - - - - -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 fab60486..00000000 Binary files a/musicmeta/frontend/fonts/open-iconic.ttf and /dev/null differ diff --git a/musicmeta/frontend/fonts/open-iconic.woff b/musicmeta/frontend/fonts/open-iconic.woff deleted file mode 100755 index f9309988..00000000 Binary files a/musicmeta/frontend/fonts/open-iconic.woff and /dev/null differ 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",