1.0 of Go version

This commit is contained in:
2020-11-27 19:22:48 +01:00
parent 952c136211
commit 316c3cde93
16 changed files with 1355 additions and 19 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea

4
.idea/MusicServer.iml generated
View File

@@ -10,7 +10,9 @@
</buildTags>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>

11
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="music_dev_local@localhost" uuid="e63caf71-2b1a-400d-9cc4-4b87b1f9b807">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/music_dev_local</jdbc-url>
</data-source>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

245
.idea/workspace.xml generated
View File

@@ -1,25 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="BranchesTreeState">
<expand>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="LOCAL_ROOT" type="e8cecc67:BranchNodeDescriptor" />
</path>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
</path>
<path>
<item name="ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="REMOTE_ROOT" type="e8cecc67:BranchNodeDescriptor" />
<item name="GROUP_NODE:origin" type="e8cecc67:BranchNodeDescriptor" />
</path>
</expand>
<select />
</component>
<component name="ChangeListManager">
<list default="true" id="d639f8a9-4b6d-4957-9ba4-8361acac15ca" name="Default Changelist" comment="" />
<list default="true" id="d639f8a9-4b6d-4957-9ba4-8361acac15ca" name="Default Changelist" comment="">
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/dataSources.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Dockerfile" afterDir="false" />
<change afterPath="$PROJECT_DIR$/database.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/swagger.yaml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/go.mod" afterDir="false" />
<change afterPath="$PROJECT_DIR$/init.sh" afterDir="false" />
<change afterPath="$PROJECT_DIR$/musicFacade.go" afterDir="false" />
<change afterPath="$PROJECT_DIR$/songs/01. Opening.mp3" afterDir="false" />
<change afterPath="$PROJECT_DIR$/songs/01. Title.mp3" afterDir="false" />
<change afterPath="$PROJECT_DIR$/syncFacade.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/app.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/musicserver.go" beforeDir="false" afterPath="$PROJECT_DIR$/musicserver.go" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="GOROOT" path="/usr/local/Cellar/go/1.15.2/libexec" />
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Go File" />
</list>
</option>
</component>
<component name="GOROOT" path="/usr/local/Cellar/go/1.15.5/libexec" />
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectId" id="1ipVJv6kTmBdByNysh6NxmS21gD" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="ASKED_SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
<property name="DatabaseDriversLRU" value="postgresql" />
<property name="DefaultGoTemplateProperty" value="Go File" />
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="go.import.settings.migrated" value="true" />
<property name="last_opened_file_path" value="$USER_HOME$" />
<property name="go.tried.to.enable.integration.vgo.integrator" value="true" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/songs" />
<property name="node.js.detected.package.eslint" value="true" />
<property name="node.js.detected.package.tslint" value="true" />
<property name="node.js.path.for.package.eslint" value="project" />
<property name="node.js.path.for.package.tslint" value="project" />
<property name="node.js.selected.package.eslint" value="(autodetect)" />
<property name="node.js.selected.package.tslint" value="(autodetect)" />
<property name="restartRequiresConfirmation" value="false" />
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/songs" />
</key>
</component>
<component name="RunManager">
<configuration name="App Engine" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="MusicServer" />
@@ -31,27 +92,189 @@
<directory value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
<configuration name="Dockerfile" type="docker-deploy" factoryName="dockerfile" temporary="true" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="sansan91/music-server:2.0.0" />
<option name="buildCliOptions" value="" />
<option name="command" value="" />
<option name="containerName" value="music-server-go" />
<option name="entrypoint" value="" />
<option name="envVars">
<list>
<DockerEnvVarImpl>
<option name="name" value="DB_HOST" />
<option name="value" value="ssh.sanplex.xyz" />
</DockerEnvVarImpl>
<DockerEnvVarImpl>
<option name="name" value="DB_USERNAME" />
<option name="value" value="postgres" />
</DockerEnvVarImpl>
<DockerEnvVarImpl>
<option name="name" value="DB_PASSWORD" />
<option name="value" value="DkrSle9501" />
</DockerEnvVarImpl>
<DockerEnvVarImpl>
<option name="name" value="DB_NAME" />
<option name="value" value="music_test" />
</DockerEnvVarImpl>
</list>
</option>
<option name="portBindings">
<list>
<DockerPortBindingImpl>
<option name="containerPort" value="8080" />
<option name="hostPort" value="8080" />
</DockerPortBindingImpl>
</list>
</option>
<option name="commandLineOptions" value="" />
<option name="sourceFilePath" value="Dockerfile" />
<option name="volumeBindings">
<list>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/sorted" />
<option name="hostPath" value="$USER_HOME$/Resilio Sync/Sorterat_test" />
</DockerVolumeBindingImpl>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/doc" />
<option name="hostPath" value="$USER_HOME$/doc" />
</DockerVolumeBindingImpl>
<DockerVolumeBindingImpl>
<option name="containerPath" value="/sorterat" />
<option name="hostPath" value="$USER_HOME$/Resilio Sync/Sorterat_test" />
</DockerVolumeBindingImpl>
</list>
</option>
</settings>
</deployment>
<method v="2" />
</configuration>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
<component name="VgoProject">
<integration-enabled>true</integration-enabled>
</component>
<component name="WindowStateProjectService">
<state width="1238" height="212" key="GridCell.Tab.0.bottom" timestamp="1602604775177">
<state x="432" y="337" key="#GOROOT" timestamp="1606056684244">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="212" key="GridCell.Tab.0.bottom/0.0.1280.800@0.0.1280.800" timestamp="1602604775177" />
<state width="1238" height="212" key="GridCell.Tab.0.center" timestamp="1602604775176">
<state x="432" y="337" key="#GOROOT/0.0.1280.800@0.0.1280.800" timestamp="1606056684244" />
<state x="440" y="240" key="#Go_Modules" timestamp="1602878032740">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="212" key="GridCell.Tab.0.center/0.0.1280.800@0.0.1280.800" timestamp="1602604775176" />
<state width="1238" height="212" key="GridCell.Tab.0.left" timestamp="1602604775175">
<state x="440" y="240" key="#Go_Modules/0.0.1280.800@0.0.1280.800" timestamp="1602878032740" />
<state x="209" y="65" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1606500738678">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="212" key="GridCell.Tab.0.left/0.0.1280.800@0.0.1280.800" timestamp="1602604775175" />
<state width="1238" height="212" key="GridCell.Tab.0.right" timestamp="1602604775177">
<state x="209" y="65" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.0.1280.800@0.0.1280.800" timestamp="1606500738678" />
<state x="354" y="206" key="#com.intellij.fileTypes.FileTypeChooser" timestamp="1606327049349">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="212" key="GridCell.Tab.0.right/0.0.1280.800@0.0.1280.800" timestamp="1602604775177" />
<state x="354" y="206" key="#com.intellij.fileTypes.FileTypeChooser/0.0.1280.800@0.0.1280.800" timestamp="1606327049349" />
<state x="63" y="22" width="1120" height="704" key="CommitChangelistDialog2" timestamp="1606500771742">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state x="63" y="22" width="1120" height="704" key="CommitChangelistDialog2/0.0.1280.800@0.0.1280.800" timestamp="1606500771742" />
<state x="500" y="304" key="Github.ShareDialog" timestamp="1602604919305">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state x="500" y="304" key="Github.ShareDialog/0.0.1280.800@0.0.1280.800" timestamp="1602604919305" />
<state x="454" y="112" key="Github.UntrackedFilesDialog" timestamp="1602604943862">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state x="454" y="112" key="Github.UntrackedFilesDialog/0.0.1280.800@0.0.1280.800" timestamp="1602604943862" />
<state width="1238" height="264" key="GridCell.Tab.0.bottom" timestamp="1606334681949">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="264" key="GridCell.Tab.0.bottom/0.0.1280.800@0.0.1280.800" timestamp="1606334681949" />
<state width="846" height="212" key="GridCell.Tab.0.bottom/0.25.1280.702@0.25.1280.702" timestamp="1606330812762" />
<state width="1238" height="264" key="GridCell.Tab.0.center" timestamp="1606334681948">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="264" key="GridCell.Tab.0.center/0.0.1280.800@0.0.1280.800" timestamp="1606334681948" />
<state width="846" height="212" key="GridCell.Tab.0.center/0.25.1280.702@0.25.1280.702" timestamp="1606330812757" />
<state width="1238" height="264" key="GridCell.Tab.0.left" timestamp="1606334681947">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="264" key="GridCell.Tab.0.left/0.0.1280.800@0.0.1280.800" timestamp="1606334681947" />
<state width="846" height="212" key="GridCell.Tab.0.left/0.25.1280.702@0.25.1280.702" timestamp="1606330812754" />
<state width="1238" height="264" key="GridCell.Tab.0.right" timestamp="1606334681949">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="264" key="GridCell.Tab.0.right/0.0.1280.800@0.0.1280.800" timestamp="1606334681949" />
<state width="846" height="212" key="GridCell.Tab.0.right/0.25.1280.702@0.25.1280.702" timestamp="1606330812760" />
<state width="1238" height="264" key="GridCell.Tab.1.bottom" timestamp="1606334681952">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="264" key="GridCell.Tab.1.bottom/0.0.1280.800@0.0.1280.800" timestamp="1606334681952" />
<state width="1238" height="264" key="GridCell.Tab.1.bottom/0.25.1280.702@0.25.1280.702" timestamp="1606330741478" />
<state width="1238" height="264" key="GridCell.Tab.1.center" timestamp="1606334681950">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="264" key="GridCell.Tab.1.center/0.0.1280.800@0.0.1280.800" timestamp="1606334681950" />
<state width="1238" height="264" key="GridCell.Tab.1.center/0.25.1280.702@0.25.1280.702" timestamp="1606330741474" />
<state width="1238" height="264" key="GridCell.Tab.1.left" timestamp="1606334681950">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="264" key="GridCell.Tab.1.left/0.0.1280.800@0.0.1280.800" timestamp="1606334681950" />
<state width="1238" height="264" key="GridCell.Tab.1.left/0.25.1280.702@0.25.1280.702" timestamp="1606330741473" />
<state width="1238" height="264" key="GridCell.Tab.1.right" timestamp="1606334681951">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state width="1238" height="264" key="GridCell.Tab.1.right/0.0.1280.800@0.0.1280.800" timestamp="1606334681951" />
<state width="1238" height="264" key="GridCell.Tab.1.right/0.25.1280.702@0.25.1280.702" timestamp="1606330741474" />
<state x="491" y="322" key="VCS.ChangelistChooser" timestamp="1604854207192">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state x="491" y="322" key="VCS.ChangelistChooser/0.0.1280.800@0.0.1280.800" timestamp="1604854207192" />
<state x="546" y="264" key="com.intellij.docker.deployment.ui.DockerPortBindingsField.v173.dialogSize" timestamp="1606060189431">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state x="546" y="264" key="com.intellij.docker.deployment.ui.DockerPortBindingsField.v173.dialogSize/0.0.1280.800@0.0.1280.800" timestamp="1606060189431" />
<state x="361" y="264" width="706" height="259" key="com.intellij.docker.deployment.ui.DockerVolumeBindingsField.v173.dialogSize" timestamp="1606334614894">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state x="361" y="264" width="706" height="259" key="com.intellij.docker.deployment.ui.DockerVolumeBindingsField.v173.dialogSize/0.0.1280.800@0.0.1280.800" timestamp="1606334614894" />
<state x="483" y="173" key="refactoring.ChangeSignatureDialog" timestamp="1603014849046">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state x="483" y="173" key="refactoring.ChangeSignatureDialog/0.0.1280.800@0.0.1280.800" timestamp="1603014849046" />
<state x="305" y="124" width="670" height="676" key="search.everywhere.popup" timestamp="1602967169862">
<screen x="0" y="0" width="1280" height="800" />
</state>
<state x="305" y="124" width="670" height="676" key="search.everywhere.popup/0.0.1280.800@0.0.1280.800" timestamp="1602967169862" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<line-breakpoint enabled="true" type="DlvLineBreakpoint">
<url>file://$PROJECT_DIR$/musicFacade.go</url>
<line>139</line>
<option name="timeStamp" value="4" />
</line-breakpoint>
</breakpoints>
</breakpoint-manager>
<watches-manager>
<configuration name="GoApplicationRunConfiguration">
<watch expression="games" language="go" />
<watch expression="songQue" language="go" />
</configuration>
</watches-manager>
</component>
</project>

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM golang:1.15.5-alpine as build_go
COPY *.go go.mod /music-server/
WORKDIR /music-server
RUN go build
# Stage 2, distribution container
FROM golang:1.15.5-alpine
RUN apk add --no-cache bash
EXPOSE 8080
VOLUME /sorted
VOLUME /sorterad
VOLUME /doc
ENV DB_HOST ""
ENV DB_PORT ""
ENV DB_USERNAME ""
ENV DB_PASSWORD ""
ENV DB_NAME ""
COPY --from=build_go /music-server/MusicServer .
COPY ./doc/swagger.yaml .
COPY ./songs/ ./songs/
COPY ./init.sh .
RUN chmod 777 ./init.sh
CMD ./init.sh

View File

@@ -1,5 +1,5 @@
application: musicserver
version: 1
version: 2.0.0
runtime: go115
api_version: go1

244
database.go Normal file
View File

@@ -0,0 +1,244 @@
package main
import (
"context"
"fmt"
"github.com/jackc/pgtype"
"github.com/jackc/pgx/v4/pgxpool"
"os"
"time"
)
var dbPool *pgxpool.Pool
func initDB(host string, port int, user string, password string, dbname string) {
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+
"password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
fmt.Println(psqlInfo)
var err error
dbPool, err = pgxpool.Connect(context.Background(), psqlInfo)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
os.Exit(1)
}
var success string
err = dbPool.QueryRow(context.Background(), "select 'Successfully connected!'").Scan(&success)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
os.Exit(1)
}
fmt.Println(success)
}
func closeDb() {
dbPool.Close()
}
func getGameName(gameId int) string {
var gameName = ""
err := dbPool.QueryRow(context.Background(),
"SELECT game_name FROM game WHERE id = $1", gameId).Scan(&gameName)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
return ""
}
return gameName
}
func setGameDeletionDate() {
_, err := dbPool.Exec(context.Background(),
"UPDATE game SET deleted=$1", time.Now())
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func clearSongs(gameId int) {
if gameId == -1 {
_, err := dbPool.Exec(context.Background(), "DELETE FROM song")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
} else {
_, err := dbPool.Exec(context.Background(), "DELETE FROM song where game_id=$1", gameId)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
}
func clearGames() {
_, err := dbPool.Exec(context.Background(), "DELETE FROM game")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func updateGameName(id int, name string, path string) {
_, err := dbPool.Exec(context.Background(),
"UPDATE game SET game_name=$1, path=$2, last_changed=$3 WHERE id=$4",
name, path, time.Now(), id)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func removeDeletionDate(id int) {
_, err := dbPool.Exec(context.Background(),
"UPDATE game SET deleted=null WHERE id=$1", id)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func addSong(song SongData) {
_, err := dbPool.Exec(context.Background(),
"INSERT INTO song(game_id, song_name, path) VALUES ($1, $2, $3)",
song.gameId, song.songName, song.path)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func findSongsFromGame(id int) []SongData {
rows, err := dbPool.Query(context.Background(),
"SELECT song_name, path, times_played FROM song WHERE game_id = $1", id)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
return nil
}
var songDataList []SongData
for rows.Next() {
var songName string
var path string
var timesPlayed int
err := rows.Scan(&songName, &path, &timesPlayed)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
songDataList = append(songDataList, SongData{
gameId: id,
songName: songName,
path: path,
timesPlayed: timesPlayed,
})
}
return songDataList
}
func getIdByGameName(name string) int {
var gameId = -1
err := dbPool.QueryRow(context.Background(),
"SELECT id FROM game WHERE game_name = $1", name).Scan(&gameId)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
return -1
}
return gameId
}
func insertGame(name string, path string) int {
gameId := -1
err := dbPool.QueryRow(context.Background(),
"INSERT INTO game(game_name, path, added) VALUES ($1, $2, $3) RETURNING id",
name, path, time.Now()).Scan(&gameId)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
resetGameIdSeq()
err2 := dbPool.QueryRow(context.Background(),
"INSERT INTO game(game_name, path, added) VALUES ($1, $2, $3) RETURNING id",
name, path, time.Now()).Scan(&gameId)
if err2 != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
return -1
}
}
return gameId
}
func insertGameWithExistingId(id int, name string, path string) {
_, err := dbPool.Exec(context.Background(),
"INSERT INTO game(id, game_name, path, added) VALUES ($1, $2, $3, $4)",
id, name, path, time.Now())
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func resetGameIdSeq() {
_, err := dbPool.Query(context.Background(), "SELECT setval('game_id_seq', (SELECT MAX(id) FROM game)+1);")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func findAllGames() []GameData {
rows, err := dbPool.Query(context.Background(),
"SELECT id, game_name, added, deleted, last_changed, path, times_played, last_played, number_of_songs FROM game")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
var gameList []GameData
for rows.Next() {
var id, timesPlayed int
var numberOfSongs pgtype.Int4
var gameName, path string
var added, deleted, lastChanged, lastPlayed pgtype.Timestamp
err := rows.Scan(&id, &gameName, &added, &deleted, &lastChanged, &path, &timesPlayed, &lastPlayed, &numberOfSongs)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
}
gameList = append(gameList, GameData{
id: id,
gameName: gameName,
added: added.Time,
deleted: deleted.Time,
lastChanged: lastChanged.Time,
path: path,
timesPlayed: timesPlayed,
lastPlayed: lastPlayed.Time,
numberOfSongs: numberOfSongs.Int,
})
}
return gameList
}
func addGamePlayed(id int) {
_, err := dbPool.Exec(context.Background(),
"UPDATE game SET times_played=times_played+1, last_played=now() WHERE id=$1", id)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func addSongPlayed(id int, name string) {
_, err := dbPool.Exec(context.Background(),
"UPDATE song SET times_played=times_played+1 WHERE game_id=$1 AND song_name=$2", id, name)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Exec failed: %v\n", err)
}
}
func testf() {
rows, dbErr := dbPool.Query(context.Background(), "select game_name from game")
if dbErr != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", dbErr)
os.Exit(1)
}
for rows.Next() {
var gameName string
dbErr = rows.Scan(&gameName)
if dbErr != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", dbErr)
}
_, _ = fmt.Fprintf(os.Stderr, "%v\n", gameName)
}
}

290
doc/swagger.yaml Normal file
View File

@@ -0,0 +1,290 @@
openapi: 3.0.3
info:
version: "2.0.0"
title: "Music Server"
description: "Rebuilt the application in Go."
contact:
email: "zarnor91@gmail.com"
servers:
- url: https://music.sanplex.xyz/
description: Main (production) server
tags:
- name: "Music"
description: "Endpoints for the music"
- name: "Version"
description: "Information about the application"
- name: "Sync"
description: "Sync the database with the folders"
paths:
/music:
get:
tags:
- "Music"
summary: "Get song"
description: "Get a specified song from the que, if invalid number it will be the first or the last song in que"
operationId: "getSong"
parameters:
- name: "song"
in: "query"
schema:
type: integer
description: "The number the song has in the que"
required: true
responses:
"200":
description: "The speciefied file"
content:
audio/mpeg:
schema:
type: object
format: binary
"500":
description: "Something wnet wrong on the server"
/music/first:
get:
tags:
- "Music"
summary: "Start a match"
description: "Get a sound check song and starts a new song que"
operationId: "getFisrt"
responses:
"200":
description: "A file"
content:
audio/mpeg:
schema:
type: object
format: binary
"500":
description: "Something wnet wrong on the server"
/music/rand:
get:
tags:
- "Music"
summary: "Get random song"
description: "Takes a random song from a random game"
operationId: "getRandom"
responses:
"200":
description: "A file"
content:
audio/mpeg:
schema:
type: object
format: binary
"500":
description: "Something wnet wrong on the server"
/music/rand/low:
get:
tags:
- "Music"
summary: "Get random song"
description: "Takes a random song from a random game but increases the chans for games that haven't been played"
operationId: "getRandomLow"
responses:
"200":
description: "A file"
content:
audio/mpeg:
schema:
type: object
format: binary
"500":
description: "Something wnet wrong on the server"
/music/info:
get:
tags:
- "Music"
summary: "Get info of current song"
description: "Return the info of the song currently playing"
operationId: "getInfo"
responses:
"200":
description: "Info of current song"
content:
application/json:
schema:
$ref: '#/components/schemas/info'
"500":
description: "Something wnet wrong on the server"
/music/list:
get:
tags:
- "Music"
summary: "Gets song que"
description: "Gets a list of all songs that have been fetched"
operationId: "getList"
responses:
"200":
description: "A list"
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/list'
"500":
description: "Something went wrong on the server"
/music/next:
get:
tags:
- "Music"
summary: "Get next song"
description: "Gets next song in the song que, if there is no more a new random song will be generated."
operationId: "getNext"
responses:
"200":
description: "A file"
content:
audio/mpeg:
schema:
type: object
format: binary
"500":
description: "Something wnet wrong on the server"
/music/previous:
get:
tags:
- "Music"
summary: "Get previous song"
description: "Gets previous song in the song que, if there is no more the first song will be returns."
operationId: "getPrevious"
responses:
"200":
description: "A file"
content:
audio/mpeg:
schema:
type: object
format: binary
"500":
description: "Something wnet wrong on the server"
/music/all:
get:
tags:
- "Music"
summary: "Gets all games"
description: "Gets a list of all games that is in the databse"
operationId: "getAll"
responses:
"200":
description: "A list"
content:
application/json:
schema:
type: array
items:
type: string
example: ["God of War", "Final Fantasy VII"]
"500":
description: "Something went wrong on the server"
/music/played:
put:
tags:
- "Music"
summary: "Increase played"
description: "Increase the number of times the current game has been played"
operationId: "setPlayed"
responses:
"202":
description: "Accepted"
"401":
description: "Bad Request"
"500":
description: "Something went wrong on the server"
/music/reset:
get:
tags:
- "Music"
summary: "Resets the song que"
description: "Resets the song que for a new match"
operationId: "reset"
responses:
"200":
description: "successful operation"
"500":
description: "Something went wrong on the server"
/version:
get:
tags:
- "Version"
summary: "Returns pet inventories by status"
description: "Returns a map of status codes to quantities"
operationId: "getVersionInfo"
responses:
"200":
description: "successful operation"
content:
application/json:
schema:
$ref: '#/components/schemas/versionInfo'
/sync:
get:
tags:
- "Sync"
summary: "Sync games"
description: "Sync the database with the folders"
operationId: "sync"
responses:
"200":
description: "successful operation"
"500":
description: "Something went wrong on the server"
components:
schemas:
list:
type: object
properties:
Game:
type: string
example: God of War
Song:
type: string
example: Main Theme.mp3
CurrentlyPlaying:
type: boolean
example: true
description: Only included on the one that's playing
SongNo:
type: integer
example: 3
info:
type: object
properties:
Game:
type: string
example: God of War
GamePlayed:
type: integer
example: 10
Song:
type: string
example: Main Theme.mp3
SongPlayed:
type: integer
example: 10
SongNo:
type: integer
example: 3
versionInfo:
type: object
properties:
version:
type: string
example: 0.5.1
changelog:
type: string
example: "Added shadow to build fat jars. Added support to give property files through arguments."
history:
type: array
items:
$ref: '#/components/schemas/historyInfo'
historyInfo:
type: object
properties:
version:
type: string
example: 0.5.0
changelog:
type: string
example: "Updated kotlin version, gradle version and al libs. Added connection to database with exposed. Added new endpoints."

9
go.mod Normal file
View File

@@ -0,0 +1,9 @@
module MusicServer
go 1.15
require (
github.com/gorilla/mux v1.8.0
github.com/jackc/pgtype v1.5.0
github.com/jackc/pgx/v4 v4.9.0
)

4
init.sh Normal file
View File

@@ -0,0 +1,4 @@
#! /bin/sh
mkdir /doc
cp swagger.yaml /doc
./MusicServer

243
musicFacade.go Normal file
View File

@@ -0,0 +1,243 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"strconv"
)
var currentSong = -1
var games []GameData
var songQue []SongData
func getSoundCheckSong() string {
reset()
files, err := ioutil.ReadDir("songs")
if err != nil {
log.Fatal(err)
}
fileInfo := files[rand.Intn(len(files))]
return "songs/" + fileInfo.Name()
}
func reset() {
songQue = nil
currentSong = -1
games = findAllGames()
}
func getRandomSong() string {
if games == nil || len(games) == 0 {
games = findAllGames()
}
game := getRandomGame(games)
songs := findSongsFromGame(game.id)
song := songs[rand.Intn(len(songs))]
currentSong = len(songQue)
songQue = append(songQue, song)
return song.path
}
func getRandomSongLowChance() string {
gameList := findAllGames()
var listOfGames []GameData
var averagePlayed = getAveragePlayed(gameList)
for _, data := range gameList {
var timesToAdd = averagePlayed - data.timesPlayed
if timesToAdd <= 0 {
listOfGames = append(listOfGames, data)
} else {
for i := 0; i < timesToAdd; i++ {
listOfGames = append(listOfGames, data)
}
}
}
game := getRandomGame(listOfGames)
songs := findSongsFromGame(game.id)
song := songs[rand.Intn(len(songs))]
currentSong = len(songQue)
songQue = append(songQue, song)
return song.path
}
func getSongInfo() SongInfo {
var currentSongData = songQue[currentSong]
currentGameData := getCurrentGame(currentSongData)
return SongInfo{
Game: currentGameData.gameName,
GamePlayed: currentGameData.timesPlayed,
Song: currentSongData.songName,
SongPlayed: currentSongData.timesPlayed,
CurrentlyPlaying: true,
SongNo: currentSong,
}
}
func getPlayedSongs() []SongInfo {
var songList []SongInfo
for i, song := range songQue {
gameData := getCurrentGame(song)
songList = append(songList, SongInfo{
Game: gameData.gameName,
GamePlayed: gameData.timesPlayed,
Song: song.songName,
SongPlayed: song.timesPlayed,
CurrentlyPlaying: i == currentSong,
SongNo: i,
})
}
return songList
}
func getSong(song string) string {
currentSong, _ = strconv.Atoi(song)
if currentSong >= len(songQue) {
currentSong = len(songQue) - 1
} else if currentSong < 0 {
currentSong = 0
}
var songData = songQue[currentSong]
return songData.path
}
func getAllGames() []string {
if games == nil || len(games) == 0 {
games = findAllGames()
}
var jsonArray []string
for _, game := range games {
jsonArray = append(jsonArray, game.gameName)
}
return jsonArray
}
func setPlayed(songNumber int) {
if songQue == nil || len(songQue) == 0 || songNumber >= len(songQue) {
return
}
var songData = songQue[songNumber]
addGamePlayed(songData.gameId)
addSongPlayed(songData.gameId, songData.songName)
}
func getNextSong() string {
if currentSong == len(songQue)-1 || currentSong == -1 {
return getRandomSong()
} else {
currentSong = currentSong + 1
var songData = songQue[currentSong]
return songData.path
}
}
func getPreviousSong() string {
if currentSong == -1 || currentSong == 0 {
var songData = songQue[0]
return songData.path
} else {
currentSong = currentSong - 1
var songData = songQue[currentSong]
return songData.path
}
}
func musicHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/music" && r.Method == http.MethodGet {
song := r.URL.Query().Get("song")
if song == "" {
w.WriteHeader(http.StatusBadRequest)
_, err := fmt.Fprint(w, "song can't be empty")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
} else {
http.ServeFile(w, r, getSong(song))
}
} else if r.URL.Path == "/music/first" && r.Method == http.MethodGet {
http.ServeFile(w, r, getSoundCheckSong())
} else if r.URL.Path == "/music/reset" && r.Method == http.MethodGet {
reset()
w.WriteHeader(http.StatusOK)
} else if r.URL.Path == "/music/rand" && r.Method == http.MethodGet {
http.ServeFile(w, r, getRandomSong())
} else if r.URL.Path == "/music/rand/low" && r.Method == http.MethodGet {
http.ServeFile(w, r, getRandomSongLowChance())
} else if r.URL.Path == "/music/info" && r.Method == http.MethodGet {
w.Header().Add("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(getSongInfo())
} else if r.URL.Path == "/music/list" && r.Method == http.MethodGet {
w.Header().Add("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(getPlayedSongs())
} else if r.URL.Path == "/music/next" {
http.ServeFile(w, r, getNextSong())
} else if r.URL.Path == "/music/previous" {
http.ServeFile(w, r, getPreviousSong())
} else if r.URL.Path == "/music/all" && r.Method == http.MethodGet {
w.Header().Add("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(getAllGames())
} else if r.URL.Path == "/music/played" && r.Method == http.MethodPut {
var p Played
err := json.NewDecoder(r.Body).Decode(&p)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
setPlayed(p.song)
w.WriteHeader(http.StatusOK)
}
}
type Played struct {
song int
}
func getCurrentGame(currentSongData SongData) GameData {
for _, game := range games {
if game.id == currentSongData.gameId {
return game
}
}
return GameData{}
}
func getAveragePlayed(gameList []GameData) int {
var sum int
for _, data := range gameList {
sum += data.timesPlayed
}
return sum / len(gameList)
}
func getRandomGame(listOfGames []GameData) GameData {
return listOfGames[rand.Intn(len(listOfGames))]
}

View File

@@ -1,14 +1,59 @@
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"log"
"net/http"
"os"
"strconv"
"time"
)
func main() {
http.HandleFunc("/", indexHandler)
// Get the value of an Environment Variable
host := os.Getenv("DB_HOST")
dbPort, dbPortErr := strconv.Atoi(os.Getenv("DB_PORT"))
if dbPortErr != nil {
dbPort = 0
}
username := os.Getenv("DB_USERNAME")
password := os.Getenv("DB_PASSWORD")
dbName := os.Getenv("DB_NAME")
fmt.Printf("host: %s, dbPort: %v, username: %s, password: %s, dbName: %s\n",
host, dbPort, username, password, dbName)
if host == "" {
host = "localhost"
}
if dbPort == 0 {
dbPort = 5432
}
if username == "" {
username = "sebastian"
}
if password == "" {
password = "950100"
}
if dbName == "" {
dbName = "music_dev_local"
}
initDB(host, dbPort, username, password, dbName)
defer closeDb()
r := mux.NewRouter()
r.HandleFunc("/sync", syncHandler)
r.HandleFunc("/sync/{func}", syncHandler)
r.HandleFunc("/music", musicHandler)
r.HandleFunc("/music/{func}", musicHandler)
r.HandleFunc("/music/{func}/{func2}", musicHandler)
r.HandleFunc("/{func}", indexHandler)
http.Handle("/", r)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
@@ -20,13 +65,91 @@ func main() {
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
type VersionData struct {
Version string `json:"version"`
Changelog string `json:"changelog"`
History []VersionData `json:"history"`
}
type SongInfo struct {
Game string `json:"Game"`
GamePlayed int `json:"GamePlayed"`
Song string `json:"Song"`
SongPlayed int `json:"SongPlayed"`
CurrentlyPlaying bool `json:"CurrentlyPlaying"`
SongNo int `json:"SongNo"`
}
type GameData struct {
id int
gameName string
added time.Time
deleted time.Time
lastChanged time.Time
path string
timesPlayed int
lastPlayed time.Time
numberOfSongs int32
}
type SongData struct {
gameId int
songName string
path string
timesPlayed int
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/test" {
_, err := fmt.Fprint(w, "Testing path")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
if r.URL.Path == "/version" {
w.Header().Add("Content-Type", "application/json")
testf()
data := VersionData{Version: "2.0.0",
Changelog: "Rebuilt the application in Go.",
History: []VersionData{
{
Version: "1.2.0",
Changelog: "Made the /sync endpoint async. " +
"Fixed bug where the game list wasn't reloaded when using /reset." +
"Fixed bug where the songNo showed in the list didn't match what should be sent.",
},
{
Version: "1.1.0",
Changelog: "Added sync endpoint, don't really trust it to 100%, would say beta. " +
"Fixed bug with /next after /previous. Added /reset endpoint. " +
"Added some info more to /info and /list.",
},
{
Version: "1.0.0",
Changelog: "Added swagger documentation. Created version 1.0.",
},
{
Version: "0.5.5",
Changelog: "Added increase played endpoint.",
},
},
}
_ = json.NewEncoder(w).Encode(data)
} else if r.URL.Path == "/doc" {
http.ServeFile(w, r, "./doc/swagger.yaml")
} else if r.URL.Path == "/" {
rows, dbErr := dbPool.Query(context.Background(), "select game_name from game")
if dbErr != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", dbErr)
os.Exit(1)
}
for rows.Next() {
var gameName string
dbErr = rows.Scan(&gameName)
if dbErr != nil {
_, _ = fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", dbErr)
}
_, _ = fmt.Fprintf(w, "%v\n", gameName)
}
_, err := fmt.Fprint(w, "Hello, World!!")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)

BIN
songs/01. Opening.mp3 Executable file

Binary file not shown.

BIN
songs/01. Title.mp3 Executable file

Binary file not shown.

153
syncFacade.go Normal file
View File

@@ -0,0 +1,153 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"sort"
"strconv"
"strings"
)
func syncHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/sync" {
w.Header().Add("Content-Type", "application/json")
syncGames()
_, err := fmt.Fprint(w, "Games are synced")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
} else if r.URL.Path == "/sync/reset" {
w.Header().Add("Content-Type", "application/json")
resetDB()
_, err := fmt.Fprint(w, "Games and songs are deleted from the database")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
}
func syncGames() {
host := os.Getenv("DB_HOST")
var dir string
if host != "" {
dir = "/sorted/"
} else {
dir = "/Users/sebastian/Resilio Sync/Sorterat_test/"
}
fmt.Printf("dir: %s\n", dir)
foldersToSkip := []string{".sync"}
fmt.Println(foldersToSkip)
setGameDeletionDate()
files, err := ioutil.ReadDir(dir)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
fmt.Println(file.Name())
path := dir + file.Name() + "/"
fmt.Println(path)
innerFiles, err := ioutil.ReadDir(path)
if err != nil {
log.Fatal(err)
}
id := -1
for _, song := range innerFiles {
id = getIdFromFile(song)
if id != -1 {
break
}
}
if id == -1 {
addNewGame(file.Name(), path)
} else {
checkIfChanged(id, file.Name(), path)
checkSongs(path, id)
}
}
}
}
func getIdFromFile(file os.FileInfo) int {
name := file.Name()
if !file.IsDir() && strings.HasSuffix(name, ".id") {
name = strings.Replace(name, ".id", "", 1)
name = strings.Replace(name, ".", "", 1)
i, _ := strconv.Atoi(name)
return i
}
return -1
}
func checkIfChanged(id int, name string, path string) {
fmt.Printf("Id from file: %v\n", id)
nameFromDb := getGameName(id)
fmt.Printf("Name from file: %v\n", name)
fmt.Printf("Name from DB: %v\n", nameFromDb)
if nameFromDb == "" {
fmt.Println("Not in db")
insertGameWithExistingId(id, name, path)
fmt.Println("Added to db")
} else if name != nameFromDb {
fmt.Println("Diff name")
updateGameName(id, name, path)
}
removeDeletionDate(id)
}
func addNewGame(name string, path string) {
newId := getIdByGameName(name)
if newId == -1 {
newId = insertGame(name, path)
}
fmt.Printf("newId = %v", newId)
fileName := path + "/." + strconv.Itoa(newId) + ".id"
fmt.Printf("fileName = %v", fileName)
err := ioutil.WriteFile(fileName, nil, 0644)
if err != nil {
panic(err)
}
checkSongs(path, newId)
}
func checkSongs(gameDir string, gameId int) {
songs := make([]SongData, 0)
findSongsFromGame := findSongsFromGame(gameId)
files, err := ioutil.ReadDir(gameDir)
if err != nil {
log.Fatal(err)
}
for _, entry := range files {
path := gameDir + entry.Name()
songName := entry.Name()
if !entry.IsDir() && !strings.HasSuffix(songName, ".id") {
songs = append(songs, SongData{gameId: gameId, songName: songName, path: path})
}
}
if len(songs) != len(findSongsFromGame) {
clearSongs(gameId)
for _, song := range songs {
addSong(song)
}
}
}
func resetDB() {
clearSongs(-1)
clearGames()
}
func contains(s []string, searchTerm string) bool {
i := sort.SearchStrings(s, searchTerm)
return i < len(s) && s[i] == searchTerm
}