소스 검색

becoming a party pig

blackwine 3 주 전
부모
커밋
8eee3ee7d1
13개의 변경된 파일571개의 추가작업 그리고 136개의 파일을 삭제
  1. 28 7
      Makefile
  2. 55 18
      README.md
  3. 37 0
      amiga.go
  4. 64 0
      at-osc/main.go
  5. 92 0
      bar.go
  6. 82 0
      c64.go
  7. 5 1
      go.mod
  8. 8 0
      go.sum
  9. BIN
      images/bar_button.png
  10. 110 0
      media.go
  11. 14 0
      osc.go
  12. 61 110
      ovly.go
  13. 15 0
      rest.go

+ 28 - 7
Makefile

@@ -1,13 +1,34 @@
+service=bar
+at_osc=@osc
+
+os=linux
+arch=amd64
 dir_guard=@mkdir -p $(@D)
+party_host=xuj.local
+exec=/usr/local/bin/$(service)
+
+all: $(service) $(at_osc)
+
+$(service): $(wildcard **.go)
+	go build .
 
-all: ovly
+$(at_osc): $(wildcard at-osc/**.go)
+	go build -o "$(at_osc)" ./at-osc
 
-ovly: ovly.go
-	go build $^
 
-put: build/ovly.linux_amd64
+run: $(service)
+	./$(service)
+
+put: build/$(service).$(os)_$(arch)
+	# Move old binary out of the way and ask old instance to quit
+	ssh $(party_host) [ -f $(exec) ] "&&" mv $(exec) $(exec).retired "||" /bin/true
+	scp $^ $(party_host):$(exec)
+	osc-utility message --host $(party_host) --port 9137 --address /bar/halt
+
+build/$(service).%: $(wildcard **.go)
+	$(dir_guard)
+	GOOS=$(word 1, $(subst _, ,$*)) GOARCH=$(word 2, $(subst _, ,$*)) go build -o $@ .
 
-build/ovly.linux_amd64: ovly.go
+build/@osc.%: $(wildcard at-osc/**.go)
 	$(dir_guard)
-	GOOS=linux GOARCH=amd64 go build -o build/ovly.linux_amd64 ovly.go
-	scp $@ xuj.local:/usr/local/bin/ovly
+	GOOS=$(word 1, $(subst _, ,$*)) GOARCH=$(word 2, $(subst _, ,$*)) go build -o $@ ./at-osc

+ 55 - 18
README.md

@@ -1,27 +1,64 @@
-API
-===
+```
+/party/bar
+```
 
-OSC API 
+or simply a **bar** is an OSC controlled demoparty ~drink~ audio/video
+mixing service.
 
-overlay
+Started from scratch as a simple idea that you can run demoparty 
+from the CLI, as a network service or by pushing MIDI buttons and tuning
+knobs.
+
+Central point of the service is...
+
+Screenbar
+---------
+
+Screenbar is a simple screen overlay for demoscene party system.
+Long bar on top of the screen draws directly from Amiga OS Screenbars.
+
+    /party/bar/title/set      - sets title bar (string)    
+    /party/bar/t/reset        - reset timer         
+
+Media player
+------------
+
+OSC controlled MPV
+
+    /party/media/play         - play media file, set path by string message
+    /party/media/stop         - stop (pause) playing media
+    /party/media/volume       - set volume for the media
+
+Emulators
 ---------
 
-    /8bus/ovly/title           - sets title bar (string)
-    /8bus/ovly/timer/reset     - reset timer (void)
+Emulators be run locally on the party machine and have similar interface with
+the default media player. They are there for convinience. You should not need
+them during the party.
+
+Real machines
+-------------
 
-player
-------
+Look into `main.go` for machine APIs.
 
-    /8bus/entry/1/play         - 
-    /8bus/entry/2/stop         - 
-    /8bus/entry/3/set_path     - 
-    /8bus/entry/next           - 
-    /8bus/entry/prev           - 
-    /8bus/entry/set            - 
-    /8bus/compo/set            - 
+C64
+---
 
-future enhancements
--------------------
+Remote controlled via 
+[REST API](https://1541u-documentation.readthedocs.io/en/latest/api/api_calls.html)
+of [1541 Ultimate-II (+)](https://1541u-documentation.readthedocs.io/en/latest/)
+FPGA based floppy disk emulator
+with [firmware](https://ultimate64.com/Firmware) version 3.11 or higher.
+Device provides access to DMA on your C64 and it can be pretty powerful.
+Keypresses can be sent via Keyboardcache (e.g. send `F1` using `POKE 631,135; POKE 198,1`)
+More info on [C64 Wiki's Keyboard page](https://www.c64-wiki.com/wiki/Keyboard#direct_addressing_of_a_key)
 
-    /8bus/ovly/timer/format    - "s": seconds, "m": minutes:seconds
+Amiga
+-----
 
+Amigas can be reset remotely using reset line of Gayle chip in
+[A600](https://www.amigawiki.org/dnl/schematics/A600_R2.pdf),
+[A1200](https://www.amigawiki.org/dnl/schematics/A1200_R1.pdf)
+[etc](https://www.amigawiki.org/doku.php?id=en:service:schematics).
+[FlashFloppy](https://github.com/keirf/flashfloppy) equipped floppy emulators (aka GOTEKs)
+can be mounted as remote, networked pendrives.

+ 37 - 0
amiga.go

@@ -0,0 +1,37 @@
+package main
+
+import (
+	"log"
+	"os/exec"
+
+	"github.com/hypebeast/go-osc/osc"
+)
+
+// Your Amiga host!
+type Amiga struct {
+	host string
+}
+
+// Reboots your Amiga hardware typically by shorting reset lines on a Gayle
+// chip.
+func (amiga *Amiga) reset(msg *osc.Message) {
+	log.Print("Amiga reset triggered at ", amiga.host)
+}
+
+// Inserts disk to a machine's df0 floppy drive.
+// Typically used to mount adf disk images remotely
+// to a flashfloppy emulated drive.
+func (amiga *Amiga) insertDisk(msg *osc.Message) {
+	filename := getOSC[string](msg)
+
+	// Find executable
+	name := "flop"
+	path, err := exec.LookPath(name)
+	if err != nil {
+		log.Printf("can't find '%s'\n", name)
+		return
+	}
+
+	// run flop command
+	exec.Command(path, filename).Run()
+}

+ 64 - 0
at-osc/main.go

@@ -0,0 +1,64 @@
+// @osc: the OSC client 
+
+package main
+
+import (
+	"log"
+	"os"
+	"strconv"
+	"github.com/hypebeast/go-osc/osc"
+)
+
+func main() {
+	// Check if we have all the required argumets to send the OSC message
+	if len(os.Args) < 2 {
+		log.New(os.Stderr, "", 0).Fatal("Usage: ", os.Args[0], " /osc/address [message...]")
+	}
+
+	host := os.Getenv("athost")
+	if host == "" {
+		host = "localhost"
+	}
+	atport := os.Getenv("atport")
+	port, err := strconv.Atoi(atport)
+	if err != nil {
+		port = 9137
+	}
+
+	client := osc.NewClient(host, port)
+	msg := osc.NewMessage(os.Args[1])
+
+	have_string := false
+	have_blob := false
+	for _, arg := range os.Args[2:] {
+		if have_string {
+			msg.Append(arg)
+			have_string = false
+		} else if arg == "-s" {
+			have_string = true
+		} else if have_blob {
+			content, err := os.ReadFile(arg)
+			if err != nil {
+                		log.Print(err)
+        		}
+			msg.Append(content)
+			have_blob = false
+		} else if arg == "-b" {
+			have_blob = true
+		} else if arg == "true" {
+			msg.Append(true)
+		} else if arg == "false" {
+			msg.Append(false)
+		} else if i, err := strconv.Atoi(arg); err == nil {
+			msg.Append(int32(i))
+		} else if f, err := strconv.ParseFloat(arg, 64); err == nil {
+			msg.Append(float32(f))
+		} else {
+			msg.Append(arg)
+		}
+	}
+
+	if err := client.Send(msg); err != nil {
+		log.Fatal(err)
+	}
+}

+ 92 - 0
bar.go

@@ -0,0 +1,92 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"time"
+
+	// bar
+	"golang.org/x/term"
+
+	// osc
+	"github.com/hypebeast/go-osc/osc"
+)
+
+// Your basic screenbar
+type ScreenBar struct {
+	title        string
+	ticks        int
+	ticker       *time.Ticker
+	tickerActive bool
+	columns      int
+}
+
+func NewScreenBar(title string) *ScreenBar {
+	bar := new(ScreenBar)
+	bar.title = title
+	bar.ticks = 0
+	bar.tickerActive = true
+
+	bar.ticker = time.NewTicker(time.Second)
+	bar.update()
+
+	// Goroutine to handle ticker updates
+	go func() {
+		for range bar.ticker.C {
+			bar.ticks += 1
+			bar.update()
+		}
+	}()
+
+	return bar
+}
+
+// (Re)paint a screen by drawing bar
+func (bar *ScreenBar) update() {
+	// Config size if in terminal
+	if term.IsTerminal(0) {
+		bar.columns, _, _ = term.GetSize(0)
+	}
+
+	// Go home and draw (type) a bar
+	fmt.Print("")
+	fmt.Print("")
+	s := fmt.Sprintf(" %%%ds %%6d ", 10-bar.columns)
+	fmt.Printf(s, bar.title, bar.ticks)
+	fmt.Print("")
+
+	// An attempt to display Screenbar button using kitty image protocol
+	cmd := exec.Command("sh", "-c", fmt.Sprintf("kitten icat -z -1 -n --place 2x1@%dx0 --stdin no images/bar_button.png < /dev/null > /dev/tty", bar.columns))
+	if out, err := cmd.Output(); err != nil {
+		os.Stdout.Write(out)
+		log.Print(err)
+	} else {
+		os.Stdout.Write(out)
+	}
+}
+
+// Set title visible on the bar
+func (bar *ScreenBar) setTitle(msg *osc.Message) {
+	for _, arg := range msg.Arguments {
+		switch arg.(type) {
+		case string:
+			bar.title = arg.(string)
+			bar.update()
+		}
+	}
+}
+
+// Ticker's gonna tick
+func (bar *ScreenBar) tick() {
+	bar.ticks += 1
+	bar.update()
+}
+
+// Ticker's gonna need to be reset sometimes
+func (bar *ScreenBar) reset(msg *osc.Message) {
+	bar.ticks = 0
+	bar.ticker.Reset(time.Second)
+	bar.update()
+}

+ 82 - 0
c64.go

@@ -0,0 +1,82 @@
+package main
+
+// c64 player dependencies
+//import "io"
+//import "os"
+//import "bytes"
+//import "mime/multipart"
+import "fmt"
+//import "net/http"
+import "net/url"
+import "strconv"
+
+import "log"
+import "github.com/hypebeast/go-osc/osc"
+
+// C64 object
+
+type C64 struct {
+	u1541_host string
+}
+
+func (c64 *C64) reset(msg *osc.Message) {
+	if _, err := put(c64.u1541_host+"/v1/machine:reset"); err != nil {
+		log.Print(err)
+	}
+}
+
+func (c64 *C64) reboot(msg *osc.Message) {
+	if _, err := put(c64.u1541_host+"/v1/machine:reboot"); err != nil {
+		log.Print(err)
+	}
+}
+
+func (c64 *C64) runPRG(msg *osc.Message) {
+	params := url.Values{}
+	params.Add("file", getOSC[string](msg))
+	if _, err := put(c64.u1541_host+"/v1/runners:run_prg?" + params.Encode()); err != nil {
+		log.Print(err)
+	}
+}
+
+func (c64 *C64) insertDisk(msg *osc.Message) {
+	params := url.Values{}
+	params.Add("image", getOSC[string](msg))
+	if _, err := put(c64.u1541_host+"/v1/drives/a:mount?" + params.Encode()); err != nil {
+		log.Print(err)
+	}
+}
+
+func (c64 *C64) sidplay(msg *osc.Message) {
+	params := url.Values{}
+	params.Add("file", getOSC[string](msg))
+	if songnr := getOSC[int](msg); songnr != 0 {
+		params.Add("songnr", strconv.Itoa(songnr))
+	}
+	if _, err := put(c64.u1541_host+"/v1/runners:sidplay?" + params.Encode()); err != nil {
+		log.Print(err)
+	}
+}
+
+// Send key strokes via Keyboardcache
+func (c64 *C64) sendKeys(msg *osc.Message) {
+	// Load buffer
+	for _, char := range getOSC[string](msg) {
+		params := url.Values{}
+		params.Add("address", "277")
+		params.Add("data", fmt.Sprintf("%02x", char))
+		log.Printf("sent %02x", char)
+		if _, err := put(c64.u1541_host+"/v1/machine:writemem?" + params.Encode()); err != nil {
+			log.Print(err)
+			return
+		}
+
+		params = url.Values{}
+		params.Add("address", "c6")
+		params.Add("data", "01")
+		if _, err := put(c64.u1541_host+"/v1/machine:writemem?" + params.Encode()); err != nil {
+			log.Print(err)
+			return
+		}
+	}
+}

+ 5 - 1
go.mod

@@ -1,7 +1,11 @@
-module 8bus
+module bar
 
 go 1.22.4
 
 require (
+	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 // indirect
+	golang.org/x/sys v0.23.0 // indirect
+	golang.org/x/term v0.23.0 // indirect
+	tinygo.org/x/drivers v0.28.0 // indirect
 )

+ 8 - 0
go.sum

@@ -1,2 +1,10 @@
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 h1:fqwINudmUrvGCuw+e3tedZ2UJ0hklSw6t8UPomctKyQ=
 github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5/go.mod h1:lqMjoCs0y0GoRRujSPZRBaGb4c5ER6TfkFKSClxkMbY=
+golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
+golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
+golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+tinygo.org/x/drivers v0.28.0 h1:ROVrGGXddmpn2+oV/Bu3LceYbtPCJckmgIqvPcN/L0k=
+tinygo.org/x/drivers v0.28.0/go.mod h1:T6snsUqS0RAxOANxiV81fQwLxDDNmprxTAYzmxoA7J0=

BIN
images/bar_button.png


+ 110 - 0
media.go

@@ -0,0 +1,110 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os/exec"
+
+	// osc
+	"github.com/hypebeast/go-osc/osc"
+)
+
+// Base structure for audiovisual media.
+// Media is simply a file containing audio and video material.
+type Media struct {
+	active bool
+	cmd    *exec.Cmd
+	args   []string
+}
+
+// Interface for Media type structs
+type MediaPlayer interface {
+	play()
+	stop()
+}
+
+// Run executable
+func run(name string, args ...string) *exec.Cmd {
+	// Find executable
+	//path, err := exec.LookPath(name)
+	//if err != nil {
+	//	log.Printf("can't find '%s'\n", name)
+	//	return nil
+	//}
+
+	//println("got path")
+	//log.Println("got path:", path, args)
+	cmd := exec.Command(name, args...)
+	if err := cmd.Run(); err != nil {
+		log.Print(err)
+		return nil
+	}
+	return cmd
+}
+
+// Run executable
+func (player *Media) run(name string, args ...string) {
+	player.cmd = exec.Command(name, args...)
+	if err := player.cmd.Run(); err != nil {
+		log.Print(err)
+	}
+}
+
+// Play media file via mpv, osc message sets the file path
+func (player *Media) play(msg *osc.Message) {
+	if player.active {
+		log.Print("please stop current player before playing another")
+		log.Print(player.cmd)
+		return
+	}
+
+	uri := getOSC[string](msg)
+
+	osc.PrintMessage(msg)
+	if uri == "" {
+		log.Print("player won't play without uri")
+		return
+	}
+	player.active = true
+
+	// you can route audio directly through alsa/hdmi but beware of clipping
+	//run(player, "mpv", "-fs", "--audio-device=alsa/hdmi", "--volume=25", uri)
+
+	args := append(player.args, uri)
+	player.run(args[0], args[1:]...)
+	player.active = false
+}
+
+// Stop the player by shutting down MPV
+func (player *Media) stop(msg *osc.Message) {
+	player.quit()
+}
+
+// Change volume using amixer command
+func (player *Media) volume(msg *osc.Message) {
+	level := getOSC[float32](msg)
+	run("amixer", "-q", "sset", "Master", fmt.Sprintf("%.2f%%", level*100.0))
+}
+
+func (player *Media) quit() {
+	if !player.active {
+		log.Print("can't kill player if it ain't running")
+		return
+	}
+	if err := player.cmd.Process.Kill(); err != nil {
+		log.Print("failed to kill process: ", err)
+	}
+	player.active = false
+}
+
+func NewMPV() *Media {
+	media := new(Media)
+	media.args = []string{"mpv", "-fs"}
+	return media
+}
+
+func NewXLEmulator() *Media {
+	media := new(Media)
+	media.args = []string{"atari800", "-stretch", "3"}
+	return media
+}

+ 14 - 0
osc.go

@@ -0,0 +1,14 @@
+package main
+
+import "github.com/hypebeast/go-osc/osc"
+
+// Generic function to get message argument of given type
+func getOSC[T any](msg *osc.Message) T {
+	var zero T
+	for _, arg := range msg.Arguments {
+		if _, isType := arg.(T); isType {
+			return arg.(T)
+		}
+	}
+	return zero
+}

+ 61 - 110
ovly.go

@@ -1,126 +1,77 @@
 package main
 
-import "fmt"
-import "log"
-import "time"
-import "os/exec"
-import "github.com/hypebeast/go-osc/osc"
+import (
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+	"syscall"
 
-// Overlay object
-type Overlay struct {
-	title	string
-	t	int
-	show_t	bool
-}
-
-//type Title struct {
-//	title	string
-//}
-//
-//type Timer struct {
-//	t	int
-//	visible	bool
-//}
-
-//type MediaPlayer interface {
-//	set() string
-//}
-
-//type Titler interface {
-//	set() string
-//}
-//
-//func (tit *Title)set(title string) {
-//	fmt.Print("")
-//	fmt.Printf("      %-160s %6d  ", title, t)
-//}
-
-// Updates title of bar in Overlay object
-func update_title(title string, t int) {
-	fmt.Print("")
-	fmt.Printf("      %-160s %6d  ", title, t)
-}
-
-// Run executable
-func run(output chan string, cmd string, args ...string) {
-	// Find executable
-	path, err := exec.LookPath(cmd)
-	if err != nil {
-		fmt.Printf("can't find '%s'\n", cmd)
-	} else {
-		fmt.Printf("'%s' executable is in '%s'\n", cmd, path)
-	}
-
-	out, err := exec.Command(path, args...).Output()
-	if err != nil {
-		log.Print(err)
-	}
-	log.Printf("%s\n", out)
-	output <- string(out)
-}
-
-// Play media file via mpv, osc message sets the file path
-func play(msg *osc.Message) {
-	var uri string;
-	ch := make(chan string)
-	for _, arg := range msg.Arguments {
-		switch arg.(type) {
-		case string:
-			uri = arg.(string)
-		}
-	}
-	run(ch, "mpv", "-fs", "--audio-device=alsa/hdmi", uri)
-}
+	"github.com/hypebeast/go-osc/osc"
+)
 
 func main() {
+	// Setup and spawn the OSC server
 	addr := "0.0.0.0:9137"
 	d := osc.NewStandardDispatcher()
-	ovly := Overlay{"DECRUNCH Screen...", 0, true}
 
-	d.AddMsgHandler("/8bus/ovly/title", func(msg *osc.Message) {
-		for _, arg := range msg.Arguments {
-			switch arg.(type) {
-			case string:
-				ovly.title = arg.(string)
-			}
-		}
-		update_title(ovly.title, 0)
-	})
-
-	ticker := time.NewTicker(time.Second)
-	d.AddMsgHandler("/8bus/ovly/timer/reset", func (msg *osc.Message) {
-		ovly.t = 0
-		ticker.Reset(time.Second)
-		update_title(ovly.title, 0)
-	})
+	// Screen bar handlers
+	bar := NewScreenBar("DECRUNCH... Screen")
+	d.AddMsgHandler("/party/bar/title/set", bar.setTitle)
+	d.AddMsgHandler("/party/bar/t/reset", bar.reset)
+
+	// Media player handlers
+	player := NewMPV()
+	d.AddMsgHandler("/party/media/play", player.play)
+	d.AddMsgHandler("/party/media/stop", player.stop)
+	d.AddMsgHandler("/party/media/volume", player.volume)
+
+	// ATARI 800 XL emulator player handlers
+	xl_emu := NewXLEmulator()
+	d.AddMsgHandler("/party/emu/xl/play", xl_emu.play)
+	d.AddMsgHandler("/party/emu/xl/stop", xl_emu.stop)
+	d.AddMsgHandler("/party/emu/xl/volume", xl_emu.volume)
+
+	// C64 handler
+	c64 := C64{u1541_host: "http://192.168.7.64"}
+	d.AddMsgHandler("/party/c64/reset", c64.reset)
+	d.AddMsgHandler("/party/c64/reboot", c64.reboot)
+	d.AddMsgHandler("/party/c64/run/sid", c64.sidplay)
+	d.AddMsgHandler("/party/c64/run/prg", c64.runPRG)
+	d.AddMsgHandler("/party/c64/drive/a/insert", c64.insertDisk)
+	d.AddMsgHandler("/party/c64/send/keys", c64.sendKeys)
+
+	// AMIGA 600 handler
+	a600 := Amiga{host: "b600.local"}
+	d.AddMsgHandler("/party/amiga/reset", a600.reset)
+	d.AddMsgHandler("/party/amiga/drive/0/insert", a600.insertDisk)
+
+	// AMIGA 1200/030 handler
+	a1230 := Amiga{host: "b1200.local"}
+	d.AddMsgHandler("/party/amiga/reset", a1230.reset)
+	d.AddMsgHandler("/party/amiga/drive/0/insert", a1230.insertDisk)
 
-	d.AddMsgHandler("/8bus/ovly/video/play", play)
+	// Disable cursor, move it home and clear the terminal
+	fmt.Print("[?25l")
 
-	// Main loop
-	//done := make(chan bool)
+	// Set up and start OSC server
 	go func() {
-		for {
-			select {
-			//case <-done:
-			//	return
-			case <-ticker.C:
-				ovly.t += 1
-				update_title(ovly.title, ovly.t)
-			}
+		server := &osc.Server{Addr: addr, Dispatcher: d}
+		if err := server.ListenAndServe(); err != nil {
+			log.Fatal("error: ", err.Error())
 		}
 	}()
 
-	// Disable cursor, move it home and clear the terminal
-	fmt.Print("[?25l")
-
-	// Set up and start OSC server
-	server := &osc.Server{
-		Addr:       addr,
-		Dispatcher: d,
-	}
-	server.ListenAndServe()
+	// Run until signals
+	ch := make(chan os.Signal, 1)
+	signal.Notify(
+		ch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT,
+	)
+	d.AddMsgHandler("/party/halt", func(msg *osc.Message) {
+		ch <- syscall.SIGINT
+	})
+	<-ch
 
-	// Enable cursor
-	fmt.Print("[?25h")
+	// Defered enable cursor (futile)
+	fmt.Println("[?25h")
 }
-

+ 15 - 0
rest.go

@@ -0,0 +1,15 @@
+package main
+
+import (
+ "log"
+ "net/http"
+)
+
+func put(url string) (resp *http.Response, err error) {
+	req, err := http.NewRequest("PUT", url, nil)
+	if err != nil {
+		log.Print(err)
+		return
+	}
+	return http.DefaultClient.Do(req)
+}