Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
AndreKR committed Jul 31, 2017
0 parents commit 3db39f6
Show file tree
Hide file tree
Showing 9 changed files with 440 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/lock
/output
/skype-media-saver.exe
/skype-media-saver.log
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Skype Media Saver

## What is this?

There is a Skype setting called "When I receive a file..." where you can select a folder and Skype used to put all
received files into that directory.

At some point Skype stopped doing that for images and videos. Those are now **only in the cloud** (and I don't know
for how long) unless you explicitly save each picture manually.

So this little tool will watch the Skype media cache folder and whenever a new file is received, it will copy this file
to its output folder.

## How to use?

Download the [latest release](). Put it in a folder and make sure it can write to this folder. Put a link in the Start >
All Programs > Startup folder. Run it.

It will write every received picture to that folder.

Currently it only works on Windows because I don't know where Linux Skype puts its files. PRs welcome.

## How to exit?

Run it again, it will ask if you want it to exit.

## How to build?

```
go get
go build -ldflags "-s -w -H=windowsgui"
```

(`-s -w` for smaller executables, `-H=windowsgui` for no console window)

### What is `rsrc.syso`?

It will be linked by the go compiler to [tell windows to use modern windows buttons](https://msdn.microsoft.com/en-us/library/windows/desktop/bb773175(v=vs.85).aspx)
in the dialogs. It can be generated from `beautiful_buttons.manifest.xml` with
`rsrc -manifest=beautiful_dialogs.manifest.xml` (by [rsrc]()).
22 changes: 22 additions & 0 deletions beautiful_dialogs.manifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="skype-media-saver"
type="win32"
/>
<description>skype-media-saver</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>
86 changes: 86 additions & 0 deletions dialogs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package main

import (
"syscall"
"unsafe"
)

const (
MB_OK = 0x00000000
MB_OKCANCEL = 0x00000001
MB_ABORTRETRYIGNORE = 0x00000002
MB_YESNOCANCEL = 0x00000003
MB_YESNO = 0x00000004
MB_RETRYCANCEL = 0x00000005
MB_CANCELTRYCONTINUE = 0x00000006
MB_ICONHAND = 0x00000010
MB_ICONQUESTION = 0x00000020
MB_ICONEXCLAMATION = 0x00000030
MB_ICONASTERISK = 0x00000040
MB_USERICON = 0x00000080
MB_ICONWARNING = MB_ICONEXCLAMATION
MB_ICONERROR = MB_ICONHAND
MB_ICONINFORMATION = MB_ICONASTERISK
MB_ICONSTOP = MB_ICONHAND

MB_DEFBUTTON1 = 0x00000000
MB_DEFBUTTON2 = 0x00000100
MB_DEFBUTTON3 = 0x00000200
MB_DEFBUTTON4 = 0x00000300
)

const (
IDOK = 1
IDCANCEL = 2
IDABORT = 3
IDRETRY = 4
IDIGNORE = 5
IDYES = 6
IDNO = 7
IDCLOSE = 8
IDHELP = 9
IDTRYAGAIN = 10
IDCONTINUE = 11
IDTIMEOUT = 32000
)

func showMessage(title string, text string, error bool) int {

// https://msdn.microsoft.com/en-us/library/windows/desktop/ms645505(v=vs.85).aspx

user32 := syscall.MustLoadDLL("user32.dll")
msgboxf := user32.MustFindProc("MessageBoxW")

icon := MB_ICONINFORMATION
if error {
icon = MB_ICONERROR
}

//noinspection GoDeprecation
ret, _, _ := msgboxf.Call(
uintptr(0),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
uintptr(icon))

return int(ret)

}

func askMessage(title string, text string) (yes bool, res int) {

// https://msdn.microsoft.com/en-us/library/windows/desktop/ms645505(v=vs.85).aspx

user32 := syscall.MustLoadDLL("user32.dll")
msgboxf := user32.MustFindProc("MessageBoxW")

//noinspection GoDeprecation
ret, _, _ := msgboxf.Call(
uintptr(0),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))),
uintptr(MB_ICONQUESTION|MB_YESNO))

return ret == IDYES, int(ret)

}
23 changes: 23 additions & 0 deletions file_copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"io"
"os"
)

func copyFile(dst, src string) error {
s, err := os.Open(src)
if err != nil {
return err
}
defer s.Close()
d, err := os.Create(dst)
if err != nil {
return err
}
if _, err := io.Copy(d, s); err != nil {
d.Close()
return err
}
return d.Close()
}
193 changes: 193 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package main

import (
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"github.com/theckman/go-flock"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
)

var exeDir = getExeDir()

var log = makeLogger()

func main() {

log.Infoln("Start")

fl := flock.NewFlock(filepath.Join(exeDir, "lock"))
locked, _ := fl.TryLock() // ignore errors because a failed lock is also an error
if !locked {
// Another instance is already running

pl := findMyOtherProcesses()
if len(pl) != 1 {
log.Fatalln("You are running two instances of Skype Media Saver from two different folders. In this case I can't help you to kill those instances, use the Task Manager.")
os.Exit(1)
}

yes, _ := askMessage("Skype Media Saver", "Another instance is already running (PID: "+strconv.Itoa(pl[0])+"). Do you want to kill it?")

if yes {
p, err := os.FindProcess(pl[0])
if err != nil {
log.Fatalln("Error getting the other process: " + err.Error())
}
err = p.Kill()
if err != nil {
log.Fatalln("Error killing the other process: " + err.Error())
}
}

os.Exit(0)
}

log.Infoln("Finding Skype media cache directories")

mcs := findSkypeMediaCaches()

if len(mcs) == 0 {
log.Warnln("No media cache directory found")
os.Exit(1)
}

for _, d := range mcs {
go watch(d)
}

select {} // block main goroutine

}

func watch(dir SkypeProfileDirectory) {

var err error

log := log.WithField("profile", dir.Name)
log.Infoln("Watching: " + dir.MediaCache)

output := filepath.Join(exeDir, "output", dir.Name)
err = os.MkdirAll(output, 0777)
if err != nil {
log.Errorln("Can't create output directory: " + err.Error())
}
log.Infoln("Output directory: " + output)

watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Errorln("Watching error: " + err.Error())
}

err = watcher.Add(dir.MediaCache)
if err != nil {
log.Errorln("Watching error: " + err.Error())
}

sync(dir.MediaCache, output)

go func() {
for err := range watcher.Errors {
log.Warnln("Error from fsnotify: " + err.Error())
}
}()

for e := range watcher.Events {

// When Skype downloads an image, the following events happen:
// Create: ^foo..._fullsize_distr
// Write: ^foo..._fullsize_distr
// Write: ^foo..._fullsize_distr
// ...
// Create: ^foo..._fullsize_distr.jpg
// Write: ^foo..._fullsize_distr.jpg

// So when we get a write event with a file extension, we assume the file to be complete

if e.Op&fsnotify.Write == fsnotify.Write {
if strings.HasSuffix(e.Name, "_fullsize_distr.jpg") ||
strings.HasSuffix(e.Name, "_fullsize_distr.png") ||
strings.HasSuffix(e.Name, "_video_distr.mp4") {

name := filepath.Base(e.Name)
err = copyFile(filepath.Join(output, name), e.Name)
if err != nil {
log.Errorln("Error copying file: " + err.Error())
}
}
}

}
}

func sync(from string, to string) {

files, err := ioutil.ReadDir(from)
if err != nil {
log.Fatalln("Error doing initial sync: " + err.Error())
}
for _, f := range files {

if f.IsDir() {
continue
}
if !(strings.HasSuffix(f.Name(), "_fullsize_distr.jpg") ||
strings.HasSuffix(f.Name(), "_fullsize_distr.png") ||
strings.HasSuffix(f.Name(), "_video_distr.mp4")) {
continue
}

dst, err := os.Stat(filepath.Join(to, f.Name()))
if err != nil || dst.ModTime().Before(f.ModTime()) {
err = copyFile(filepath.Join(to, f.Name()), filepath.Join(from, f.Name()))
if err != nil {
log.Errorln("Error copying file: " + err.Error())
}
}
}

}

func getExeDir() string {
if os.Getenv("EXEDIR") != "" {
return os.Getenv("EXEDIR")
} else {
return filepath.Dir(os.Args[0])
}
}

func makeLogger() *logrus.Logger {
log := logrus.New()

log.SetLevel(logrus.DebugLevel)

log.Hooks.Add(DialogHook{})

f, err := os.OpenFile(exeDir+"/skype-media-saver.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
log.Fatalln("Can't open log file: " + err.Error())
}

log.Out = f

return log
}

type DialogHook struct{}

func (DialogHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.WarnLevel,
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}
}

func (DialogHook) Fire(e *logrus.Entry) error {
showMessage("Skype Media Saver", e.Message, true)
return nil
}
Loading

0 comments on commit 3db39f6

Please sign in to comment.