├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.adoc ├── build.gradle ├── docker-compose.yml ├── docs ├── rediscogs-architecture-gdi.svg └── rediscogs-architecture-search.svg ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── manifest.yml ├── rediscogs-api ├── .gitignore ├── build.gradle └── src │ └── main │ ├── java │ └── com │ │ └── redislabs │ │ └── rediscogs │ │ ├── ImageRepository.java │ │ ├── LikeConsumer.java │ │ ├── LikeService.java │ │ ├── RediscogsApplication.java │ │ ├── RediscogsController.java │ │ ├── RediscogsProperties.java │ │ ├── SearchService.java │ │ ├── WebSocketConfig.java │ │ └── model │ │ ├── Album.java │ │ ├── Like.java │ │ └── User.java │ └── resources │ └── application.properties ├── rediscogs-ui ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── browserslist ├── build.gradle ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── proxy.conf.json ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── favorites │ │ │ ├── favorites.component.css │ │ │ ├── favorites.component.html │ │ │ ├── favorites.component.spec.ts │ │ │ └── favorites.component.ts │ │ ├── material.module.ts │ │ ├── search.service.spec.ts │ │ ├── search.service.ts │ │ └── search │ │ │ ├── search.component.css │ │ │ ├── search.component.html │ │ │ ├── search.component.spec.ts │ │ │ └── search.component.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── favicon.ico │ │ ├── redisearch.svg │ │ ├── redislabs.png │ │ └── redislabs.svg │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.d.ts │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ └── theme.scss ├── start-ng.sh ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json └── settings.gradle /.dockerignore: -------------------------------------------------------------------------------- 1 | docs 2 | rediscogs-ui/node_modules 3 | rediscogs-ui/dist 4 | rediscogs-ui/build 5 | rediscogs-api/build 6 | .git 7 | .gitignore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.settings/ 3 | /.project 4 | /.gradle/ 5 | .idea 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:13-jdk-slim AS build 2 | WORKDIR /app 3 | COPY . /app 4 | RUN ./gradlew build --no-daemon 5 | 6 | #### Stage 2: A minimal docker image with command to run the app 7 | FROM openjdk:13-jdk-slim 8 | 9 | EXPOSE 8080 10 | 11 | RUN mkdir /app 12 | 13 | COPY --from=build /app/rediscogs-api/build/libs/*.jar /app/rediscogs-api.jar 14 | 15 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/rediscogs-api.jar"] -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Rediscogs 2 | // Settings 3 | :idprefix: 4 | :idseparator: - 5 | ifdef::env-github,env-browser[:outfilesuffix: .adoc] 6 | :toc: preamble 7 | endif::[] 8 | ifndef::env-github[:icons: font] 9 | // URIs 10 | :project-repo: Redislabs-Solution-Architects/rediscogs 11 | :uri-repo: https://github.com/{project-repo} 12 | // GitHub customization 13 | ifdef::env-github[] 14 | :badges: 15 | :tag: master 16 | :!toc-title: 17 | :tip-caption: :bulb: 18 | :note-caption: :paperclip: 19 | :important-caption: :heavy_exclamation_mark: 20 | :caution-caption: :fire: 21 | :warning-caption: :warning: 22 | endif::env-github[] 23 | 24 | Redis demo based on data from https://data.discogs.com[discogs.com]. 25 | 26 | == Run the demo 27 | 28 | [source,shell] 29 | ---- 30 | git clone https://github.com/Redislabs-Solution-Architects/rediscogs.git 31 | cd rediscogs 32 | docker-compose up 33 | ---- 34 | 35 | Access the demo at http://localhost[] 36 | 37 | TIP: You will need a https://www.discogs.com/developers[Discogs API token] to have album covers displayed. + 38 | Use the following environment variable to pass it to Rediscogs: + 39 | [source,shell] 40 | ---- 41 | export DISCOGS_API_TOKEN= 42 | docker-compose up 43 | ---- 44 | 45 | 46 | == Demo Steps 47 | 48 | === RediSearch 49 | . Launch `redis-cli` 50 | . Show number of documents in RediSearch index: 51 | + 52 | `FT.INFO masters` 53 | . Run simple keyword search: 54 | + 55 | `FT.SEARCH masters java` 56 | + 57 | TIP: `title` is a phonetic text field so you will notice results containing words that sound similar 58 | . Run prefix search: 59 | + 60 | `FT.SEARCH masters spring*` 61 | . Open http://localhost[] 62 | . Enter some characters in the Artist field to retrieve suggestions from RediSearch (e.g. `Dusty`) 63 | . Select an artist from the auto-complete options and click on the `Submit` button 64 | . Refine the search by adding a numeric filter on release year in `Query` field: 65 | + 66 | `@year:[1960 1970]` 67 | . Refine the search further by adding a filter on release genres: 68 | + 69 | `@year:[1960 1970] @genres:{pop | rock}` 70 | 71 | === Caching 72 | . Select a different artist and hit `Submit` 73 | . Notice how long it takes to load images from the https://api.discogs.com[Discogs API] 74 | . After all images have been loaded, click on the `Submit` button again 75 | . Notice how fast the images are loading this time around 76 | . In `redis-cli` show cached images: 77 | + 78 | `KEYS "images::*"` 79 | . Show type of a cached image: 80 | + 81 | `TYPE "images::319832"` 82 | . Display image bytes stored in String data structure: 83 | + 84 | `GET "images::319832"` 85 | 86 | === Session Store 87 | . Enter your name in the top right section of the page 88 | . Choose an artist and hit `Submit` 89 | . Click `like` on some of the returned albums 90 | . Hit `Submit` again to refresh the list of albums 91 | . Notice how your likes are kept in the current session 92 | . In `redis-cli` show session-related keys: 93 | + 94 | `KEYS "spring:session:*"` 95 | . Choose a session entry and show its content: 96 | + 97 | `HGETALL "spring:session:sessions:d1e08957-6cee-49b6-81af-b21720d3c372"` 98 | 99 | === Redis Streams 100 | . Open http://localhost/#/likes[] in another browser window, side-by-side with the previous one 101 | . In the search page click `like` on any album. Notice the likes showing up in real-time in the other browser window 102 | . In a terminal window listen for messages on the stream: 103 | + 104 | [source,shell] 105 | ---- 106 | $ while true; do redis-cli XREAD BLOCK 0 STREAMS likes:stream $; done 107 | ... 108 | 5) 1) "1557884829631-0" 109 | 2) 1) "_class" 110 | 2) "com.redislabs.rediscogs.model.LikeMessage" 111 | 3) "album.id" 112 | 4) "171410" 113 | 5) "album.artist" 114 | 6) "Lalo Schifrin" 115 | 7) "album.artistId" 116 | 8) "23165" 117 | 9) "album.title" 118 | 10) "Bullitt (Original Motion Picture Soundtrack)" 119 | 11) "album.year" 120 | 12) "1968" 121 | 13) "album.like" 122 | 14) "0" 123 | 15) "album.genres.[0]" 124 | 16) "Jazz" 125 | 17) "album.genres.[1]" 126 | 18) "Stage & Screen" 127 | 19) "album.genres.[2]" 128 | 20) "Soundtrack" 129 | 21) "album.genres.[3]" 130 | 22) "Smooth Jazz" 131 | 23) "album.genres.[4]" 132 | 24) "Jazz-Funk" 133 | 25) "user.name" 134 | 26) "Julien" 135 | 27) "userAgent" 136 | 28) "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Safari/605.1.15" 137 | 29) "time" 138 | 30) "2019-05-15T01:47:09.629678Z" 139 | ---- 140 | . In redis-cli show the stats being maintained off the stream 141 | [source,shell] 142 | ---- 143 | 127.0.0.1:6379> zrevrange stats:album 0 3 WITHSCORES 144 | 1) "You Don't Love Me" 145 | 2) "3" 146 | 3) "No. 1 In Your Heart" 147 | 4) "2" 148 | 5) "Bullitt (Original Motion Picture Soundtrack)" 149 | 6) "1" 150 | ---- 151 | 152 | == Architecture 153 | 154 | === Getting Data In™ 155 | 156 | Discogs.com makes monthly dumps of their whole database available for download: https://data.discogs.com[data.discogs.com]. The data is in XML format and formatted according to the discogs.com http://www.discogs.com/developers/[API spec]. 157 | 158 | For example the masters XML file looks like this: 159 | [source,xml] 160 | ``` 161 | 162 | 163 | ... 164 | 165 | 166 | 167 | 168 | 8887 169 | Parliament 170 | 171 | 172 | 173 | Funk / Soul 174 | 175 | 176 | 177 | 178 | 1977 179 | Funkentelechy Vs. The Placebo Syndrome 180 | Correct 181 | 182 | ... 183 | 184 | ``` 185 | 186 | The ReDiscogs app streams in that Masters XML file using https://spring.io/projects/spring-batch[Spring Batch]: 187 | 188 | {empty} + 189 | 190 | image::https://redislabs-solution-architects.github.io/rediscogs/rediscogs-architecture-gdi.svg[Getting Data In] 191 | 192 | {empty} + 193 | 194 | On the RediSearch side, the `masters` index has the following schema created using the https://oss.redislabs.com/redisearch/Commands.html#ftcreate[`FT.CREATE`] command: 195 | 196 | - `artist`: Text field 197 | - `artistId`: Tag field 198 | - `genres`: Tag field 199 | - `title`: Phonetic Text field 200 | - `year`: Numeric field 201 | 202 | 203 | Each `master` entry (i.e. album) is stored in RediSearch under that index using the https://oss.redislabs.com/redisearch/Commands.html#ftadd[`FT.ADD`] command. 204 | 205 | === Search 206 | 207 | The data loaded previously is searchable via an Angular front-end accessing Spring Web services: 208 | 209 | {empty} + 210 | 211 | image::https://redislabs-solution-architects.github.io/rediscogs/rediscogs-architecture-search.svg[Search] 212 | 213 | {empty} + 214 | 215 | Queries submitted by the user translate into a REST API call that in turn calls the https://oss.redislabs.com/redisearch/Commands.html#ftsearch[`FT.SEARCH`] command. 216 | 217 | For each master returned from the search, ReDiscogs fetches the corresponding album cover image from the https://www.discogs.com/developers/[Discogs API] and caches it in Redis using https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html#boot-features-caching-provider-redis[Spring Cache]. Any album later returned by another search will have its image served from cache instead of the API, making access much faster and cheaper (the Discogs API is throttled at 60 calls per minute). 218 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | group = 'com.redislabs' 3 | version = '1.0.0-SNAPSHOT' 4 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | server: 5 | build: 6 | context: ./ 7 | dockerfile: Dockerfile 8 | restart: always 9 | ports: 10 | - "80:8080" 11 | depends_on: 12 | - redisearch 13 | environment: 14 | SPRING_REDIS_HOST: redisearch 15 | STOMP_PORT: 80 16 | DISCOGS_API_TOKEN: ${DISCOGS_API_TOKEN} 17 | 18 | redisearch: 19 | image: redislabs/redisearch:latest 20 | restart: always 21 | ports: 22 | - "6379:6379" 23 | -------------------------------------------------------------------------------- /docs/rediscogs-architecture-gdi.svg: -------------------------------------------------------------------------------- 1 | 2 |
Albums
Albums
RediSearch
RediSearch
Spring
Batch
[Not supported by viewer]
-------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redislabs-Solution-Architects/rediscogs/3d112988f6e8d5e4615d37e1aed54cec4cea275a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | applications: 2 | - name: rediscogs 3 | memory: 1024M 4 | path: brewdis-api/build/libs/rediscogs-api-1.0.0-SNAPSHOT.jar 5 | services: 6 | - rediscogs_redis 7 | -------------------------------------------------------------------------------- /rediscogs-api/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | 11 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 12 | !/.mvn/wrapper/maven-wrapper.jar 13 | .DS_Store 14 | /.settings/ 15 | /.classpath 16 | /.project 17 | /.mvn/ 18 | /.factorypath 19 | /build/ 20 | /bin/ 21 | -------------------------------------------------------------------------------- /rediscogs-api/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.2.4.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.9.RELEASE' 4 | id 'java' 5 | id 'com.github.ben-manes.versions' version '0.27.0' 6 | } 7 | 8 | sourceCompatibility = '1.8' 9 | 10 | repositories { 11 | mavenCentral() 12 | mavenLocal() 13 | } 14 | 15 | dependencies { 16 | implementation 'org.springframework.boot:spring-boot-starter-web' 17 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 18 | implementation 'org.springframework.boot:spring-boot-starter-cache' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive' 20 | implementation 'org.springframework.session:spring-session-data-redis' 21 | implementation 'org.ruaux:jdiscogs:1.1.3' 22 | compileOnly 'org.projectlombok:lombok' 23 | annotationProcessor 'org.projectlombok:lombok' 24 | implementation project(path: ':rediscogs-ui', configuration: 'default') 25 | testImplementation('org.springframework.boot:spring-boot-starter-test') { 26 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' 27 | } 28 | } 29 | 30 | test { 31 | useJUnitPlatform() 32 | } -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/ImageRepository.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | import java.net.MalformedURLException; 7 | import java.net.URL; 8 | 9 | import org.ruaux.jdiscogs.api.DiscogsClient; 10 | import org.ruaux.jdiscogs.api.model.Image; 11 | import org.ruaux.jdiscogs.api.model.Master; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.cache.annotation.Cacheable; 14 | import org.springframework.stereotype.Component; 15 | 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | @Component 19 | @Slf4j 20 | public class ImageRepository { 21 | 22 | @Autowired 23 | private RediscogsProperties config; 24 | @Autowired 25 | private DiscogsClient discogs; 26 | 27 | @Cacheable(value = "images", unless = "#result == null") 28 | public byte[] getImage(String masterId) { 29 | if (config.getImageDelay() > 0) { 30 | try { 31 | Thread.sleep(config.getImageDelay()); 32 | } catch (InterruptedException e) { 33 | log.warn("Sleep interrupted", e); 34 | return null; 35 | } 36 | } 37 | Master response = discogs.getMaster(masterId); 38 | if (response != null && response.getImages() != null && response.getImages().size() > 0) { 39 | Image image = response.getPrimaryImage(); 40 | if (image == null) { 41 | image = response.getImages().get(0); 42 | } 43 | String uriString = image.getUri(); 44 | try { 45 | URL url = new URL(uriString); 46 | try (BufferedInputStream in = new BufferedInputStream(url.openStream()); 47 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { 48 | byte dataBuffer[] = new byte[1024]; 49 | int bytesRead; 50 | while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { 51 | outputStream.write(dataBuffer, 0, bytesRead); 52 | } 53 | return outputStream.toByteArray(); 54 | } catch (IOException e) { 55 | log.error("Could not read stream from URL: {}", url, e); 56 | } 57 | } catch (MalformedURLException e) { 58 | log.error("Invalid URL: {}", uriString); 59 | } 60 | } 61 | return null; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/LikeConsumer.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs; 2 | 3 | import java.time.Duration; 4 | 5 | import javax.annotation.PreDestroy; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.data.redis.connection.stream.StreamOffset; 9 | import org.springframework.data.redis.connection.stream.StreamReadOptions; 10 | import org.springframework.data.redis.core.StringRedisTemplate; 11 | import org.springframework.messaging.simp.SimpMessageSendingOperations; 12 | import org.springframework.stereotype.Component; 13 | 14 | import com.redislabs.rediscogs.model.Like; 15 | 16 | import lombok.Setter; 17 | 18 | @Component 19 | public class LikeConsumer extends Thread { 20 | 21 | @Autowired 22 | private RediscogsProperties config; 23 | @Setter 24 | private boolean stopped; 25 | @Autowired 26 | private SimpMessageSendingOperations sendingOperations; 27 | @Autowired 28 | private StringRedisTemplate template; 29 | 30 | @SuppressWarnings("unchecked") 31 | @Override 32 | public void run() { 33 | StreamReadOptions options = StreamReadOptions.empty().block(Duration.ofMillis(100)); 34 | StreamOffset offset = StreamOffset.latest(config.getLikesStream()); 35 | while (!stopped) { 36 | template.opsForStream().read(Like.class, options, offset).forEach(r -> { 37 | Like like = r.getValue(); 38 | sendingOperations.convertAndSend(config.getStomp().getLikesTopic(), like); 39 | template.opsForZSet().incrementScore(config.getAlbumStatsKey(), like.getAlbum().getTitle(), 1); 40 | template.opsForZSet().incrementScore(config.getArtistStatsKey(), like.getAlbum().getArtist(), 1); 41 | template.opsForZSet().incrementScore(config.getUserAgentStatsKey(), like.getUserAgent(), 1); 42 | }); 43 | } 44 | } 45 | 46 | @PreDestroy 47 | public void shutdown() { 48 | setStopped(true); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/LikeService.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs; 2 | 3 | import java.time.Instant; 4 | import java.util.stream.Stream; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.domain.Range; 8 | import org.springframework.data.redis.connection.RedisZSetCommands.Limit; 9 | import org.springframework.data.redis.connection.stream.ObjectRecord; 10 | import org.springframework.data.redis.connection.stream.RecordId; 11 | import org.springframework.data.redis.core.StringRedisTemplate; 12 | import org.springframework.stereotype.Component; 13 | 14 | import com.redislabs.rediscogs.model.Album; 15 | import com.redislabs.rediscogs.model.Like; 16 | import com.redislabs.rediscogs.model.User; 17 | 18 | import lombok.extern.slf4j.Slf4j; 19 | 20 | @Component 21 | @Slf4j 22 | public class LikeService { 23 | 24 | @Autowired 25 | private RediscogsProperties config; 26 | @Autowired 27 | private StringRedisTemplate template; 28 | 29 | public Stream likes() { 30 | return template.opsForStream().reverseRange(Like.class, config.getLikesStream(), Range.unbounded(), 31 | Limit.limit().count(config.getMaxLikes())).stream().map(m -> m.getValue()); 32 | } 33 | 34 | public void like(Album album, User user, String userAgent) { 35 | Like like = Like.builder().album(album).user(user).userAgent(userAgent).time(Instant.now()).build(); 36 | RecordId recordId = template.opsForStream().add(ObjectRecord.create(config.getLikesStream(), like)); 37 | log.info("Liked album {} - RecordId: {}", like.getAlbum().getId(), recordId.getValue()); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/RediscogsApplication.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs; 2 | 3 | import org.ruaux.jdiscogs.data.JDiscogsBatchConfiguration; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.ApplicationArguments; 6 | import org.springframework.boot.ApplicationRunner; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.cache.annotation.EnableCaching; 10 | 11 | @SpringBootApplication 12 | @EnableCaching 13 | public class RediscogsApplication implements ApplicationRunner { 14 | 15 | @Autowired 16 | private JDiscogsBatchConfiguration batch; 17 | @Autowired 18 | private LikeConsumer likeConsumer; 19 | 20 | public static void main(String[] args) { 21 | SpringApplication.run(RediscogsApplication.class, args); 22 | } 23 | 24 | @Override 25 | public void run(ApplicationArguments args) throws Exception { 26 | batch.runJobs(); 27 | likeConsumer.start(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/RediscogsController.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.util.LinkedHashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.Stream; 10 | 11 | import javax.servlet.http.HttpServletResponse; 12 | import javax.servlet.http.HttpSession; 13 | 14 | import org.apache.tomcat.util.http.fileupload.IOUtils; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.http.CacheControl; 17 | import org.springframework.http.HttpHeaders; 18 | import org.springframework.http.HttpStatus; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.http.ResponseEntity; 21 | import org.springframework.web.bind.annotation.CrossOrigin; 22 | import org.springframework.web.bind.annotation.GetMapping; 23 | import org.springframework.web.bind.annotation.PathVariable; 24 | import org.springframework.web.bind.annotation.PostMapping; 25 | import org.springframework.web.bind.annotation.RequestBody; 26 | import org.springframework.web.bind.annotation.RequestHeader; 27 | import org.springframework.web.bind.annotation.RequestMapping; 28 | import org.springframework.web.bind.annotation.RequestParam; 29 | import org.springframework.web.bind.annotation.RestController; 30 | 31 | import com.redislabs.rediscogs.RediscogsProperties.StompConfig; 32 | import com.redislabs.rediscogs.model.Album; 33 | import com.redislabs.rediscogs.model.Like; 34 | import com.redislabs.rediscogs.model.User; 35 | 36 | import lombok.Builder; 37 | import lombok.Data; 38 | import lombok.RequiredArgsConstructor; 39 | import lombok.extern.slf4j.Slf4j; 40 | 41 | @RestController 42 | @RequiredArgsConstructor 43 | @RequestMapping(path = "/api") 44 | @CrossOrigin 45 | @Slf4j 46 | @SuppressWarnings("unchecked") 47 | class RediscogsController { 48 | 49 | @Autowired 50 | private RediscogsProperties config; 51 | @Autowired 52 | private ImageRepository imageRepository; 53 | @Autowired 54 | private LikeService likeService; 55 | @Autowired 56 | private SearchService searchService; 57 | 58 | @GetMapping("/config/stomp") 59 | public StompConfig stompConfig() { 60 | return config.getStomp(); 61 | } 62 | 63 | @Data 64 | @Builder 65 | public static class ArtistSuggestion { 66 | private String name; 67 | private String id; 68 | } 69 | 70 | @GetMapping("/suggest/artists") 71 | public Stream suggestArtists( 72 | @RequestParam(name = "prefix", defaultValue = "", required = false) String prefix) { 73 | return searchService.suggestArtists(prefix); 74 | } 75 | 76 | @PostMapping("/likes/album") 77 | public ResponseEntity like(@RequestBody Album album, HttpSession session, 78 | @RequestHeader(HttpHeaders.USER_AGENT) String userAgent) { 79 | Set likes = (Set) session.getAttribute(config.getLikesAttribute()); 80 | if (likes == null) { 81 | likes = new LinkedHashSet<>(); 82 | } 83 | likes.add(album.getId()); 84 | session.setAttribute(config.getLikesAttribute(), likes); 85 | likeService.like(album, (User) session.getAttribute(config.getUserAttribute()), userAgent); 86 | return new ResponseEntity<>(HttpStatus.OK); 87 | } 88 | 89 | @PostMapping("/user") 90 | public ResponseEntity setUsername(@RequestBody User user, HttpSession session) { 91 | log.info("Setting user '{}'", user.getName()); 92 | session.setAttribute(config.getUserAttribute(), user); 93 | return new ResponseEntity<>(HttpStatus.OK); 94 | } 95 | 96 | @GetMapping("/user") 97 | public User user(HttpSession session) { 98 | return (User) session.getAttribute(config.getUserAttribute()); 99 | } 100 | 101 | @GetMapping("/likes") 102 | public Stream likes() { 103 | return likeService.likes(); 104 | } 105 | 106 | @GetMapping("/search/albums") 107 | public List searchAlbums(HttpSession session, 108 | @RequestParam(name = "query", required = false, defaultValue = "") String query) { 109 | List albums = searchService.searchAlbums(query).collect(Collectors.toList()); 110 | Set likes = (Set) session.getAttribute(config.getLikesAttribute()); 111 | if (likes != null) { 112 | albums.forEach(album -> album.setLike(likes.contains(album.getId()))); 113 | } 114 | return albums; 115 | } 116 | 117 | @GetMapping(value = "/image/album/{id}") 118 | public void getImageAsResource(@PathVariable("id") String masterId, HttpServletResponse response) 119 | throws IOException { 120 | final HttpHeaders headers = new HttpHeaders(); 121 | headers.setCacheControl(CacheControl.noCache().getHeaderValue()); 122 | response.setContentType(MediaType.IMAGE_JPEG_VALUE); 123 | byte[] buffer = imageRepository.getImage(masterId); 124 | if (buffer != null) { 125 | IOUtils.copy(new ByteArrayInputStream(buffer), response.getOutputStream()); 126 | } 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/RediscogsProperties.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import lombok.Data; 10 | 11 | @Configuration 12 | @ConfigurationProperties(prefix = "") 13 | @EnableAutoConfiguration 14 | @Data 15 | public class RediscogsProperties { 16 | 17 | private int searchResultsLimit = 20; 18 | private long imageDelay = 3000; 19 | private String userAttribute = "username"; 20 | private String likesAttribute = "likes"; 21 | private String likesStream = "likes:stream"; 22 | private int maxLikes = 10; 23 | private StompConfig stomp = new StompConfig(); 24 | private boolean fuzzySuggest = true; 25 | private String anonymousUsername = "Anonymous Coward"; 26 | private String albumStatsKey = "stats:album"; 27 | private String artistStatsKey = "stats:artist"; 28 | private String userAgentStatsKey = "stats:user-agent"; 29 | 30 | @Data 31 | public static class StompConfig implements Serializable { 32 | private static final long serialVersionUID = 706007058202655483L; 33 | private String protocol = "ws"; 34 | private String host = "localhost"; 35 | private int port = 8080; 36 | private String endpoint = "/websocket"; 37 | private String destinationPrefix = "/topic"; 38 | private String likesTopic = destinationPrefix + "/likes"; 39 | 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/SearchService.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs; 2 | 3 | import java.util.List; 4 | import java.util.stream.Stream; 5 | 6 | import org.ruaux.jdiscogs.data.JDiscogsBatchProperties; 7 | import org.ruaux.jdiscogs.data.MasterIndexWriter; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.redislabs.lettusearch.StatefulRediSearchConnection; 12 | import com.redislabs.lettusearch.search.Direction; 13 | import com.redislabs.lettusearch.search.Limit; 14 | import com.redislabs.lettusearch.search.SearchOptions; 15 | import com.redislabs.lettusearch.search.SearchResults; 16 | import com.redislabs.lettusearch.search.SortBy; 17 | import com.redislabs.lettusearch.suggest.SuggestGetOptions; 18 | import com.redislabs.lettusearch.suggest.SuggestResult; 19 | import com.redislabs.rediscogs.RediscogsController.ArtistSuggestion; 20 | import com.redislabs.rediscogs.model.Album; 21 | 22 | @Component 23 | public class SearchService { 24 | 25 | @Autowired 26 | private RediscogsProperties config; 27 | @Autowired 28 | private JDiscogsBatchProperties props; 29 | @Autowired 30 | private StatefulRediSearchConnection connection; 31 | 32 | public Stream suggestArtists(String prefix) { 33 | List> results = connection.sync().sugget(props.getArtistSuggestionIndex(), prefix, 34 | SuggestGetOptions.builder().withPayloads(true).max(20l).fuzzy(config.isFuzzySuggest()).build()); 35 | return results.stream().map(s -> ArtistSuggestion.builder().name(s.string()).id(s.payload()).build()); 36 | } 37 | 38 | public Stream searchAlbums(String query) { 39 | SearchResults results = connection.sync().search(props.getMasterIndex(), query, 40 | SearchOptions.builder().limit(Limit.builder().num(config.getSearchResultsLimit()).build()) 41 | .sortBy(SortBy.builder().field("year").direction(Direction.Ascending).build()).build()); 42 | return results.stream().map(r -> { 43 | String[] genres = r.getOrDefault(MasterIndexWriter.FIELD_GENRES, "").split(props.getHashArrayDelimiter()); 44 | return Album.builder().id(r.documentId()).artist(r.get(MasterIndexWriter.FIELD_ARTIST)) 45 | .artistId(r.get(MasterIndexWriter.FIELD_ARTISTID)).title(r.get(MasterIndexWriter.FIELD_TITLE)) 46 | .year(r.get(MasterIndexWriter.FIELD_YEAR)).genres(genres).build(); 47 | }); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 6 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 7 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 8 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 9 | 10 | @Configuration 11 | @EnableWebSocketMessageBroker 12 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 13 | 14 | @Autowired 15 | private RediscogsProperties config; 16 | 17 | @Override 18 | public void configureMessageBroker(MessageBrokerRegistry registry) { 19 | registry.enableSimpleBroker(config.getStomp().getDestinationPrefix()); 20 | registry.setApplicationDestinationPrefixes("/app"); 21 | } 22 | 23 | @Override 24 | public void registerStompEndpoints(StompEndpointRegistry registry) { 25 | registry.addEndpoint(config.getStomp().getEndpoint()).setAllowedOrigins("*"); 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/model/Album.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Data 7 | @Builder 8 | public class Album { 9 | 10 | private String id; 11 | private String artist; 12 | private String artistId; 13 | private String title; 14 | private String year; 15 | private boolean like; 16 | private String[] genres; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/model/Like.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs.model; 2 | 3 | import java.time.Instant; 4 | 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.ToString; 8 | 9 | @Data 10 | @Builder 11 | @ToString(of = "album") 12 | public class Like { 13 | 14 | Album album; 15 | User user; 16 | String userAgent; 17 | Instant time; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /rediscogs-api/src/main/java/com/redislabs/rediscogs/model/User.java: -------------------------------------------------------------------------------- 1 | package com.redislabs.rediscogs.model; 2 | 3 | import java.io.Serializable; 4 | 5 | import lombok.Data; 6 | 7 | @Data 8 | public class User implements Serializable { 9 | 10 | private static final long serialVersionUID = -8200810671621141323L; 11 | private String name; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /rediscogs-api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #logging.level.root=DEBUG 2 | spring.cache.cache-names=images 3 | spring.cache.redis.time-to-live=600000 4 | discogs.data.jobs=masterindex -------------------------------------------------------------------------------- /rediscogs-ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /rediscogs-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | /node/ 41 | /target/ 42 | /yarn.lock 43 | /ui-src/ 44 | /build/ 45 | /.gradle/ 46 | -------------------------------------------------------------------------------- /rediscogs-ui/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.1.5. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /rediscogs-ui/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "rediscogs-ui": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | 13 | }, 14 | "architect": { 15 | "build": { 16 | "builder": "@angular-devkit/build-angular:browser", 17 | "options": { 18 | "outputPath": "dist/rediscogs-ui", 19 | "index": "src/index.html", 20 | "main": "src/main.ts", 21 | "polyfills": "src/polyfills.ts", 22 | "tsConfig": "tsconfig.app.json", 23 | "assets": [ 24 | "src/favicon.ico", 25 | "src/assets" 26 | ], 27 | "styles": [ 28 | "src/styles.css", 29 | "src/theme.scss" 30 | ], 31 | "scripts": [] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "fileReplacements": [ 36 | { 37 | "replace": "src/environments/environment.ts", 38 | "with": "src/environments/environment.prod.ts" 39 | } 40 | ], 41 | "optimization": true, 42 | "outputHashing": "all", 43 | "sourceMap": false, 44 | "extractCss": true, 45 | "namedChunks": false, 46 | "aot": true, 47 | "extractLicenses": true, 48 | "vendorChunk": false, 49 | "buildOptimizer": true, 50 | "budgets": [ 51 | { 52 | "type": "initial", 53 | "maximumWarning": "2mb", 54 | "maximumError": "5mb" 55 | }, 56 | { 57 | "type": "anyComponentStyle", 58 | "maximumWarning": "6kb", 59 | "maximumError": "10kb" 60 | } 61 | ] 62 | } 63 | } 64 | }, 65 | "serve": { 66 | "builder": "@angular-devkit/build-angular:dev-server", 67 | "options": { 68 | "browserTarget": "rediscogs-ui:build" 69 | }, 70 | "configurations": { 71 | "production": { 72 | "browserTarget": "rediscogs-ui:build:production" 73 | } 74 | } 75 | }, 76 | "extract-i18n": { 77 | "builder": "@angular-devkit/build-angular:extract-i18n", 78 | "options": { 79 | "browserTarget": "rediscogs-ui:build" 80 | } 81 | }, 82 | "test": { 83 | "builder": "@angular-devkit/build-angular:karma", 84 | "options": { 85 | "main": "src/test.ts", 86 | "polyfills": "src/polyfills.ts", 87 | "tsConfig": "tsconfig.spec.json", 88 | "karmaConfig": "karma.conf.js", 89 | "assets": [ 90 | "src/favicon.ico", 91 | "src/assets" 92 | ], 93 | "styles": [ 94 | "src/styles.css", 95 | "src/theme.scss" 96 | ], 97 | "scripts": [] 98 | } 99 | }, 100 | "lint": { 101 | "builder": "@angular-devkit/build-angular:tslint", 102 | "options": { 103 | "tsConfig": [ 104 | "tsconfig.app.json", 105 | "tsconfig.spec.json", 106 | "e2e/tsconfig.json" 107 | ], 108 | "exclude": [ 109 | "**/node_modules/**" 110 | ] 111 | } 112 | }, 113 | "e2e": { 114 | "builder": "@angular-devkit/build-angular:protractor", 115 | "options": { 116 | "protractorConfig": "e2e/protractor.conf.js", 117 | "devServerTarget": "brewdis-ui:serve" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "devServerTarget": "brewdis-ui:serve:production" 122 | } 123 | } 124 | } 125 | } 126 | } 127 | }, 128 | "defaultProject": "rediscogs-ui" 129 | } -------------------------------------------------------------------------------- /rediscogs-ui/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /rediscogs-ui/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'com.github.node-gradle.node' version '2.2.0' 4 | } 5 | 6 | node { 7 | version = '13.6.0' 8 | npmVersion = '6.13.4' 9 | download = true 10 | } 11 | 12 | jar.dependsOn 'npm_run_build' 13 | 14 | jar { 15 | from 'dist/rediscogs-ui' into 'static' 16 | } 17 | -------------------------------------------------------------------------------- /rediscogs-ui/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /rediscogs-ui/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('client app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /rediscogs-ui/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /rediscogs-ui/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /rediscogs-ui/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/client'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /rediscogs-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rediscogs-ui", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --proxy-config proxy.conf.json", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~8.2.14", 15 | "@angular/cdk": "^8.2.3", 16 | "@angular/common": "~8.2.14", 17 | "@angular/compiler": "~8.2.14", 18 | "@angular/core": "~8.2.14", 19 | "@angular/flex-layout": "^8.0.0-beta.27", 20 | "@angular/forms": "~8.2.14", 21 | "@angular/material": "^8.2.3", 22 | "@angular/platform-browser": "~8.2.14", 23 | "@angular/platform-browser-dynamic": "~8.2.14", 24 | "@angular/router": "~8.2.14", 25 | "@stomp/ng2-stompjs": "^7.2.0", 26 | "hammerjs": "^2.0.8", 27 | "rxjs": "~6.5.4", 28 | "tslib": "^1.10.0", 29 | "zone.js": "~0.9.1" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "~0.803.24", 33 | "@angular/cli": "~8.3.24", 34 | "@angular/compiler-cli": "~8.2.14", 35 | "@angular/language-service": "~8.2.14", 36 | "@types/node": "~8.9.4", 37 | "@types/jasmine": "~3.3.8", 38 | "@types/jasminewd2": "~2.0.3", 39 | "codelyzer": "^5.0.0", 40 | "jasmine-core": "~3.4.0", 41 | "jasmine-spec-reporter": "~4.2.1", 42 | "karma": "~4.1.0", 43 | "karma-chrome-launcher": "~2.2.0", 44 | "karma-coverage-istanbul-reporter": "~2.0.1", 45 | "karma-jasmine": "~2.0.1", 46 | "karma-jasmine-html-reporter": "^1.4.0", 47 | "protractor": "~5.4.0", 48 | "ts-node": "~7.0.0", 49 | "tslint": "~5.15.0", 50 | "typescript": "~3.5.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rediscogs-ui/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:8080/api", 4 | "secure": false, 5 | "changeOrigin": true, 6 | "pathRewrite": {"^/api" : ""} 7 | } 8 | } -------------------------------------------------------------------------------- /rediscogs-ui/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { SearchAlbumsComponent } from './search/search.component'; 4 | import { FavoriteAlbumsComponent } from './favorites/favorites.component'; 5 | 6 | const routes: Routes = [ 7 | {path: '', pathMatch: 'full', redirectTo: 'search'}, 8 | {path: 'search' , component: SearchAlbumsComponent}, 9 | {path: 'likes', component: FavoriteAlbumsComponent} 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AppRoutingModule { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | color: white; 3 | } 4 | 5 | .menu-spacer { 6 | flex: 1 1 auto; 7 | } 8 | 9 | .main { 10 | height: 3em; 11 | } 12 | 13 | .logo { 14 | height: 100%; 15 | margin-right: 5px; 16 | } 17 | 18 | .home { 19 | font-size: 2em; 20 | } 21 | 22 | .redislabs { 23 | height: 3em; 24 | } 25 | 26 | .link { 27 | font-size: 1em; 28 | } 29 | 30 | mat-toolbar-row { 31 | margin-top: 3px; 32 | margin-bottom: 3px; 33 | } -------------------------------------------------------------------------------- /rediscogs-ui/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 18 |
19 | 20 | Redis Labs 21 | 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'client'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('client'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to client!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'ReDiscogs'; 10 | } 11 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | import { HashLocationStrategy, LocationStrategy } from '@angular/common'; 5 | import { HttpClientModule } from '@angular/common/http'; 6 | import { AppRoutingModule } from './app-routing.module'; 7 | import { AppComponent } from './app.component'; 8 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 9 | import { MatDialogModule } from '@angular/material/dialog'; 10 | import { MatPaginatorModule } from '@angular/material/paginator'; 11 | 12 | import { 13 | MatButtonModule, MatIconModule, MatCardModule, 14 | MatInputModule, MatAutocompleteModule, MatListModule, 15 | MatGridListModule, MatToolbarModule, MatSelectModule, 16 | MatTableModule, MatSortModule, MatButtonToggleModule, MatExpansionModule 17 | } from '@angular/material'; 18 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 19 | import { MatTooltipModule } from '@angular/material/tooltip'; 20 | import { MaterialModule } from './material.module'; 21 | import { FavoriteAlbumsComponent } from './favorites/favorites.component'; 22 | import { SearchAlbumsComponent } from './search/search.component'; 23 | 24 | @NgModule({ 25 | declarations: [ 26 | AppComponent, 27 | SearchAlbumsComponent, 28 | FavoriteAlbumsComponent 29 | ], 30 | imports: [ 31 | BrowserModule, 32 | BrowserAnimationsModule, 33 | MatTooltipModule, 34 | MaterialModule, 35 | AppRoutingModule, 36 | HttpClientModule, 37 | ReactiveFormsModule, 38 | FormsModule, 39 | FlexLayoutModule, 40 | MatButtonModule, 41 | MatIconModule, 42 | MatCardModule, 43 | MatInputModule, 44 | MatAutocompleteModule, 45 | MatListModule, 46 | MatGridListModule, 47 | MatToolbarModule, 48 | MatSelectModule, 49 | MatTableModule, 50 | MatSortModule, 51 | MatButtonToggleModule, 52 | MatDialogModule, 53 | MatPaginatorModule, 54 | MatExpansionModule 55 | ], 56 | providers: [{provide: LocationStrategy, useClass: HashLocationStrategy }], 57 | bootstrap: [AppComponent] 58 | }) 59 | export class AppModule { } 60 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/favorites/favorites.component.css: -------------------------------------------------------------------------------- 1 | .album-image { 2 | height: 150px; 3 | } 4 | 5 | .album-image img { 6 | width: 80px; 7 | height: 80px; 8 | } 9 | 10 | .like-list { 11 | display: flex; 12 | flex-flow: row wrap; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/favorites/favorites.component.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/favorites/favorites.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FavoriteAlbumsComponent } from './favorite-albums.component'; 4 | 5 | describe('FavoriteAlbumsComponent', () => { 6 | let component: FavoriteAlbumsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FavoriteAlbumsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FavoriteAlbumsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/favorites/favorites.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { StompService, StompConfig } from '@stomp/ng2-stompjs'; 3 | import { HttpClient } from '@angular/common/http'; 4 | 5 | @Component({ 6 | selector: 'app-favorites', 7 | templateUrl: './favorites.component.html', 8 | styleUrls: ['./favorites.component.css'] 9 | }) 10 | export class FavoriteAlbumsComponent implements OnInit { 11 | 12 | API_URL = '/api/'; 13 | 14 | private stompService: StompService; 15 | private likes = []; 16 | 17 | constructor(private http: HttpClient) { } 18 | 19 | ngOnInit() { 20 | this.http.get(this.API_URL + 'likes').subscribe((likes: any) => this.likes = likes); 21 | this.http.get(this.API_URL + 'config/stomp').subscribe((stomp: any) => this.connectStompService(stomp)); 22 | } 23 | 24 | connectStompService(config: any) { 25 | const stompUrl = config.protocol + '://' + config.host + ':' + config.port + config.endpoint; 26 | const stompConfig: StompConfig = { 27 | url: stompUrl, 28 | headers: { 29 | login: '', 30 | passcode: '' 31 | }, 32 | heartbeat_in: 0, 33 | heartbeat_out: 20000, 34 | reconnect_delay: 5000, 35 | debug: true 36 | }; 37 | this.stompService = new StompService(stompConfig); 38 | this.stompService.subscribe(config.likesTopic).subscribe(like => this.likes.unshift(JSON.parse(like.body))); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { 4 | MatSidenavModule, 5 | MatToolbarModule, 6 | MatIconModule, 7 | MatListModule, 8 | } from '@angular/material'; 9 | 10 | @NgModule({ 11 | imports: [MatSidenavModule, 12 | MatToolbarModule, 13 | MatIconModule, 14 | MatListModule], 15 | exports: [MatSidenavModule, 16 | MatToolbarModule, 17 | MatIconModule, 18 | MatListModule] 19 | }) 20 | export class MaterialModule { } 21 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/search.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { SearchService } from './search.service'; 4 | 5 | describe('SearchService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [SearchService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([SearchService], (service: SearchService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/search.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class SearchService { 9 | 10 | API_URL = '/api/'; 11 | 12 | constructor(private http: HttpClient) { } 13 | 14 | suggestArtists(prefix: string): Observable { 15 | let params = new HttpParams(); 16 | if (prefix != null) { 17 | params = params.set('prefix', prefix); 18 | } 19 | return this.http.get(this.API_URL + 'suggest/artists', { params }); 20 | } 21 | 22 | searchAlbums(artistId: string, query: string): Observable { 23 | let params = new HttpParams(); 24 | if (artistId != null) { 25 | params = params.set('artistId', artistId); 26 | } 27 | if (query != null) { 28 | params = params.set('query', query); 29 | } 30 | return this.http.get(this.API_URL + 'search/albums', { params }); 31 | } 32 | 33 | likeAlbum(album: any) { 34 | const options = { 35 | headers: new HttpHeaders({ 36 | 'Content-Type': 'application/json' 37 | }) 38 | }; 39 | this.http.post(this.API_URL + 'likes/album', album, options).subscribe( 40 | (val) => { 41 | }, 42 | response => { 43 | console.log('POST call in error', response); 44 | }, 45 | () => { 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/search/search.component.css: -------------------------------------------------------------------------------- 1 | .user-form { 2 | display: flex; 3 | height: 50px; 4 | margin-left: 75%; 5 | } 6 | 7 | .user-field { 8 | flex: 18%; 9 | margin: 1%; 10 | } 11 | 12 | .search-form { 13 | display: flex; 14 | height: 50px; 15 | margin: 2%; 16 | align-items: center; 17 | } 18 | 19 | .artist-field { 20 | flex: 18%; 21 | margin: 1%; 22 | } 23 | 24 | .search-field { 25 | flex: 60%; 26 | margin: 1%; 27 | } 28 | 29 | .search-results { 30 | width: 100%; 31 | margin-left: 2% 32 | } 33 | 34 | .text-align-right { 35 | text-align: right; 36 | } 37 | 38 | .album-card { 39 | position: relative; 40 | margin-bottom: 10px; 41 | margin-right: 2%; 42 | align-self: flex-end; 43 | width: 18%; 44 | } 45 | 46 | .album-card img { 47 | width: 90%; 48 | } 49 | 50 | .like-button-overlay { 51 | display: inline-block; 52 | position: absolute; 53 | top: -5px; 54 | left: -5px; 55 | } 56 | 57 | .card-content { 58 | margin: 0px; 59 | padding: 0px; 60 | } 61 | 62 | .row { 63 | display: flex; 64 | white-space: nowrap; 65 | overflow: hidden; 66 | text-overflow: ellipsis; 67 | } 68 | 69 | .row h5 { 70 | margin-top: 0px; 71 | margin-bottom: 2px; 72 | } 73 | 74 | .album-details { 75 | margin-bottom: 2px; 76 | font-size: small; 77 | } -------------------------------------------------------------------------------- /rediscogs-ui/src/app/search/search.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 | 11 | 12 | {{artist.name }} | ID: {{artist.id}} 13 | 14 | 15 | 16 | 17 | 20 | 21 | 24 |
25 |
26 | 27 |
29 |
30 | {{album.title}} 31 | 34 |
35 |
36 |
{{album.title}} ({{album.year}})
37 |
38 |
{{album.genres}}
39 |
40 |
41 |
-------------------------------------------------------------------------------- /rediscogs-ui/src/app/search/search.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SearchAlbumsComponent } from './search-albums.component'; 4 | 5 | describe('SearchAlbumsComponent', () => { 6 | let component: SearchAlbumsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SearchAlbumsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SearchAlbumsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /rediscogs-ui/src/app/search/search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { HttpClient, HttpParams, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; 3 | import { SearchService } from '../search.service'; 4 | import { Observable } from 'rxjs'; 5 | import { ReactiveFormsModule, FormControl, FormsModule } from '@angular/forms'; 6 | import { 7 | map, 8 | debounceTime, 9 | distinctUntilChanged, 10 | switchMap, 11 | tap 12 | } from 'rxjs/operators'; 13 | 14 | @Component({ 15 | selector: 'app-search-albums', 16 | templateUrl: './search.component.html', 17 | styleUrls: ['./search.component.css'] 18 | }) 19 | export class SearchAlbumsComponent implements OnInit { 20 | API_URL = '/api/'; 21 | title = 'ReDiscogs'; 22 | private userField: FormControl; 23 | private results: Observable; 24 | private artists: Observable; 25 | private searchField: FormControl; 26 | private artistField: FormControl; 27 | 28 | constructor(private http: HttpClient, private searchService: SearchService) { } 29 | 30 | ngOnInit() { 31 | this.userField = new FormControl(); 32 | this.getUsername().subscribe((res) => { 33 | if (res) { 34 | this.userField.setValue(res.name); 35 | } 36 | }); 37 | this.userField.valueChanges.pipe(debounceTime(300)).subscribe(name => this.setUsername(this.userField.value)); 38 | this.searchField = new FormControl(); 39 | this.artistField = new FormControl(); 40 | this.artistField.valueChanges.pipe( 41 | debounceTime(300) 42 | ).subscribe(prefix => this.searchService.suggestArtists(this.artistField.value).subscribe(data => { this.artists = data; })); 43 | } 44 | 45 | getUsername(): Observable { 46 | return this.http.get(this.API_URL + 'user'); 47 | } 48 | 49 | setUsername(username: string) { 50 | const options = { 51 | headers: new HttpHeaders({ 52 | 'Content-Type': 'application/json' 53 | }) 54 | }; 55 | const user = { 56 | name: username 57 | }; 58 | this.http.post(this.API_URL + 'user', user, options).subscribe( 59 | (val) => { 60 | }, 61 | response => { 62 | console.log('POST call in error', response); 63 | }, 64 | () => { 65 | }); 66 | } 67 | 68 | artistSelected(artist: any) { 69 | this.searchField.setValue('@artistId:{' + artist.id + '} '); 70 | } 71 | 72 | like(album: any) { 73 | this.searchService.likeAlbum(album); 74 | album.like = true; 75 | } 76 | 77 | displayFn(artist: any) { 78 | if (artist) { return artist.name; } 79 | } 80 | 81 | search() { 82 | let artistId = null; 83 | if (this.artistField.value != null) { 84 | artistId = this.artistField.value.id; 85 | } 86 | this.results = this.searchService.searchAlbums(artistId, this.searchField.value); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /rediscogs-ui/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redislabs-Solution-Architects/rediscogs/3d112988f6e8d5e4615d37e1aed54cec4cea275a/rediscogs-ui/src/assets/.gitkeep -------------------------------------------------------------------------------- /rediscogs-ui/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redislabs-Solution-Architects/rediscogs/3d112988f6e8d5e4615d37e1aed54cec4cea275a/rediscogs-ui/src/assets/favicon.ico -------------------------------------------------------------------------------- /rediscogs-ui/src/assets/redisearch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 19 | 26 | 29 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /rediscogs-ui/src/assets/redislabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redislabs-Solution-Architects/rediscogs/3d112988f6e8d5e4615d37e1aed54cec4cea275a/rediscogs-ui/src/assets/redislabs.png -------------------------------------------------------------------------------- /rediscogs-ui/src/assets/redislabs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /rediscogs-ui/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /rediscogs-ui/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /rediscogs-ui/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Redislabs-Solution-Architects/rediscogs/3d112988f6e8d5e4615d37e1aed54cec4cea275a/rediscogs-ui/src/favicon.ico -------------------------------------------------------------------------------- /rediscogs-ui/src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'googlemaps'; -------------------------------------------------------------------------------- /rediscogs-ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReDiscogs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /rediscogs-ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /rediscogs-ui/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | import 'hammerjs/hammer'; 64 | -------------------------------------------------------------------------------- /rediscogs-ui/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~@angular/material/prebuilt-themes/deeppurple-amber.css"; 3 | @import 'https://fonts.googleapis.com/icon?family=Material+Icons'; 4 | 5 | body { 6 | font-family: Roboto, "Helvetica Neue", sans-serif; 7 | margin: 0; 8 | } -------------------------------------------------------------------------------- /rediscogs-ui/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /rediscogs-ui/src/theme.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | // Plus imports for other components in your app. 3 | 4 | // Include the common styles for Angular Material. We include this here so that you only 5 | // have to load a single css file for Angular Material in your app. 6 | // Be sure that you only ever include this mixin once! 7 | @include mat-core(); 8 | 9 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 10 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 11 | // hue. Available color palettes: https://material.io/design/color/ 12 | 13 | $rediscogs-app-primary: mat-palette($mat-amber, 900, 500, 100); 14 | $rediscogs-app-accent: mat-palette($mat-orange, A400, A200, A700); 15 | 16 | // The warn palette is optional (defaults to red). 17 | $rediscogs-app-warn: mat-palette($mat-orange); 18 | 19 | // Create the theme object (a Sass map containing all of the palettes). 20 | $rediscogs-app-theme: mat-light-theme($rediscogs-app-primary, $rediscogs-app-accent, $rediscogs-app-warn); 21 | 22 | // Include theme styles for core and each component used in your app. 23 | // Alternatively, you can import and @include the theme mixins for each component 24 | // that you are using. 25 | @include angular-material-theme($rediscogs-app-theme); -------------------------------------------------------------------------------- /rediscogs-ui/start-ng.sh: -------------------------------------------------------------------------------- 1 | ng serve --proxy-config proxy.conf.json -------------------------------------------------------------------------------- /rediscogs-ui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ], 14 | "angularCompilerOptions": { 15 | "enableIvy": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rediscogs-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rediscogs-ui/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ], 18 | "angularCompilerOptions": { 19 | "enableIvy": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rediscogs-ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-var-requires": false, 64 | "object-literal-key-quotes": [ 65 | true, 66 | "as-needed" 67 | ], 68 | "object-literal-sort-keys": false, 69 | "ordered-imports": false, 70 | "quotemark": [ 71 | true, 72 | "single" 73 | ], 74 | "trailing-comma": false, 75 | "no-conflicting-lifecycle": true, 76 | "no-host-metadata-property": true, 77 | "no-input-rename": true, 78 | "no-inputs-metadata-property": true, 79 | "no-output-native": true, 80 | "no-output-on-prefix": true, 81 | "no-output-rename": true, 82 | "no-outputs-metadata-property": true, 83 | "template-banana-in-box": true, 84 | "template-no-negated-async": true, 85 | "use-lifecycle-interface": true, 86 | "use-pipe-transform-interface": true 87 | }, 88 | "rulesDirectory": [ 89 | "codelyzer" 90 | ] 91 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'rediscogs' 2 | 3 | include 'rediscogs-api', 'rediscogs-ui' 4 | --------------------------------------------------------------------------------