├── .gitignore ├── CMakeCPackOptions.cmake ├── CMakeLists.txt ├── Makefile ├── README.md └── src ├── common-defs.h ├── core.c ├── core.h ├── gtk_compat.h ├── ui.c ├── ui.h ├── util.c ├── util.h ├── vk-api.c ├── vk-api.h └── vkontakte_plugin.c /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | 6 | # Compiled Dynamic libraries 7 | *.so 8 | 9 | # Compiled Static libraries 10 | *.lai 11 | *.la 12 | *.a 13 | /Debug 14 | /build 15 | /gtk-2.12 16 | /Test 17 | -------------------------------------------------------------------------------- /CMakeCPackOptions.cmake: -------------------------------------------------------------------------------- 1 | # Determine current architecture 2 | macro(dpkg_arch VAR_NAME) 3 | find_program(DPKG_PROGRAM dpkg DOC "dpkg program of Debian-based systems") 4 | if (DPKG_PROGRAM) 5 | execute_process( 6 | COMMAND ${DPKG_PROGRAM} --print-architecture 7 | OUTPUT_VARIABLE ${VAR_NAME} 8 | OUTPUT_STRIP_TRAILING_WHITESPACE 9 | ) 10 | endif(DPKG_PROGRAM) 11 | endmacro(dpkg_arch) 12 | 13 | 14 | # CPack configuration 15 | set(CPACK_PACKAGE_NAME "deadbeef-plugin-vk") 16 | set(CPACK_PACKAGE_VENDOR "https://github.com/scorpp/db-vk") 17 | set(CPACK_PACKAGE_VERSION "0.2.0") 18 | set(CPACK_PACKAGE_CONTACT "keryascorpio@gmail.com") 19 | set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "DeaDBeeF plugin for VKontakte") 20 | 21 | # DEB package config 22 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Kirill Malyshev ") 23 | set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/scorpp/db-vk") 24 | set(CPACK_DEBIAN_PACKAGE_SECTION "sound") 25 | set(CPACK_DEBIAN_PACKAGE_DEPENDS "libgtk2.0-0, libcurl3-gnutls, deadbeef, libjson-glib-1.0-0") 26 | if (${CPACK_GENERATOR} STREQUAL "DEB") 27 | set(CPACK_SET_DESTDIR true) 28 | set(CPACK_INSTALL_PREFIX /opt/deadbeef) 29 | 30 | dpkg_arch(CPACK_DEBIAN_PACKAGE_ARCHITECTURE) 31 | if (CPACK_DEBIAN_PACKAGE_ARCHITECTURE) 32 | set(CPACK_PACKAGE_FILE_NAME ${CPACK_PACKAGE_NAME}_${CPACK_PACKAGE_VERSION}_${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}) 33 | else (CPACK_DEBIAN_PACKAGE_ARCHITECTURE) 34 | set(CPACK_PACKAGE_FILE_NAME ${CPACK_PACKAGE_NAME}_${CPACK_PACKAGE_VERSION}_${CMAKE_SYSTEM_NAME}) 35 | endif (CPACK_DEBIAN_PACKAGE_ARCHITECTURE) 36 | endif (${CPACK_GENERATOR} STREQUAL "DEB") 37 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(DB_VK) 2 | cmake_minimum_required(VERSION 2.6) 3 | 4 | # Build both by default 5 | option(WITH_GTK2 "Build GTK2 version") 6 | option(WITH_GTK3 "Build GTK3 version") 7 | 8 | find_package(CURL REQUIRED) 9 | find_package(PkgConfig REQUIRED) 10 | 11 | pkg_check_modules(JANSSON REQUIRED jansson) 12 | include_directories(${JANSSON_INCLUDE_DIRS}) 13 | link_directories(${JANSSON_LIBRARY_DIRS}) 14 | add_compile_options(${JANSSON_CFLAGS}) 15 | 16 | include_directories(${DB_VK_SOURCE_DIR}) 17 | 18 | set(CMAKE_C_FLAGS "-g -Wall") 19 | 20 | file(GLOB VK_PLUGIN_SRC "src/*.h" "src/*.c") 21 | 22 | # Older versions of cmake does not support modifying INCLUDE_DIRECTORIES for a target 23 | # by means of SET_TARGET_PROPERTIES command 24 | if (CMAKE_MAJOR_VERSION EQUAL "2" 25 | AND (CMAKE_MINOR_VERSION LESS "8" OR CMAKE_MINOR_VERSION EQUAL "8") 26 | AND CMAKE_PATCH_VERSION LESS "10") 27 | set(CMAKE_PRIOR_TO_2_8_10 "ON") 28 | endif() 29 | # With this old version we can't build two versions at the same time :( 30 | if (CMAKE_PRIOR_TO_2_8_10 AND WITH_GTK2 AND WITH_GTK3) 31 | message(FATAL_ERROR "Cannot build both GTK2 & GTK3 libs at once on cmake ${CMAKE_VERSION}. Build with either version of GTK separately (e.g. -DWITH_GTK2=ON -DWITH_GTK3=OFF) or upgrade cmake to at least 2.8.10") 32 | endif() 33 | 34 | 35 | pkg_check_modules(GTK2 REQUIRED gtk+-2.0) 36 | if (WITH_GTK2 AND NOT GTK2_FOUND) 37 | message(FATAL_ERROR "GTK2 development files are not installed") 38 | endif () 39 | 40 | if (GTK2_FOUND) 41 | add_library(vkontakte_gtk2 SHARED ${VK_PLUGIN_SRC}) 42 | target_link_libraries(vkontakte_gtk2 ${GTK2_LIBRARIES} ${CURL_LIBRARIES} ${JANSSON_LIBRARIES}) 43 | 44 | if (CMAKE_PRIOR_TO_2_8_10) 45 | include_directories(${GTK2_INCLUDE_DIRS}) 46 | else() 47 | get_target_property(VK_GTK2_INCLUDE_DIRS vkontakte_gtk2 INCLUDE_DIRECTORIES) 48 | set_target_properties(vkontakte_gtk2 PROPERTIES INCLUDE_DIRECTORIES "${VK_GTK2_INCLUDE_DIRS};${GTK2_INCLUDE_DIRS}") 49 | endif() 50 | 51 | link_directories(${GTK2_LIBRARY_DIRS}) 52 | 53 | set_target_properties(vkontakte_gtk2 PROPERTIES PREFIX "") 54 | install(TARGETS vkontakte_gtk2 DESTINATION "lib${LIB_SUFFIX}/deadbeef") 55 | endif () 56 | 57 | pkg_check_modules(GTK3 gtk+-3.0) 58 | if (WITH_GTK3 AND NOT GTK3_FOUND) 59 | message(FATAL_ERROR "GTK3 development files are not installed") 60 | endif () 61 | 62 | if (GTK3_FOUND) 63 | add_library(vkontakte_gtk3 SHARED ${VK_PLUGIN_SRC}) 64 | target_link_libraries(vkontakte_gtk3 ${GTK3_LIBRARIES} ${CURL_LIBRARIES} ${JANSSON_LIBRARIES}) 65 | 66 | if (CMAKE_PRIOR_TO_2_8_10) 67 | include_directories(${GTK3_INCLUDE_DIRS}) 68 | else() 69 | get_target_property(VK_GTK3_INCLUDE_DIRS vkontakte_gtk3 INCLUDE_DIRECTORIES) 70 | set_target_properties(vkontakte_gtk3 PROPERTIES INCLUDE_DIRECTORIES "${VK_GTK3_INCLUDE_DIRS};${GTK3_INCLUDE_DIRS}") 71 | endif() 72 | 73 | link_directories(${GTK3_LIBRARY_DIRS}) 74 | 75 | set_target_properties(vkontakte_gtk3 PROPERTIES PREFIX "") 76 | install(TARGETS vkontakte_gtk3 DESTINATION "lib${LIB_SUFFIX}/deadbeef") 77 | endif () 78 | 79 | if (!GTK2_FOUND AND !GTK3_FOUND) 80 | message(FATAL_ERROR "Either GTK2 or GTK3 required") 81 | endif () 82 | 83 | # Configure packaging 84 | set(CPACK_PROJECT_CONFIG_FILE ${PROJECT_SOURCE_DIR}/CMakeCPackOptions.cmake) 85 | include(CPack) 86 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OUT_GTK2=vkontakte_gtk2.so 2 | OUT_GTK3=vkontakte_gtk3.so 3 | CC?=gcc 4 | PREFIX?=/usr/local 5 | GTK2_CFLAGS?=$(shell pkg-config --cflags gtk+-2.0 --silence-errors) 6 | GTK2_LIBS?=$(shell pkg-config --libs gtk+-2.0 --silence-errors) 7 | GTK3_CFLAGS?=$(shell pkg-config --cflags gtk+-3.0 --silence-errors) 8 | GTK3_LIBS?=$(shell pkg-config --libs gtk+-3.0 --silence-errors) 9 | JANSSON_CFLAGS?=`pkg-config --cflags jansson` 10 | JANSSON_LIBS?=`pkg-config --libs jansson` 11 | CURL_CFLAGS?=`pkg-config --cflags libcurl` 12 | CURL_LIBS?=`pkg-config --libs libcurl` 13 | CFLAGS+=-Wall -fPIC -D_GNU_SOURCE -g -O0 -std=c99 $(JANSSON_CFLAGS) $(CURL_CFLAGS) 14 | LDFLAGS+=-shared $(JANSSON_LIBS) $(CURL_LIBS) -lssl 15 | SOURCES=$(wildcard src/*.c) 16 | 17 | OBJECTS_GTK2=$(SOURCES:.c=_gtk2.o) 18 | OBJECTS_GTK3=$(SOURCES:.c=_gtk3.o) 19 | 20 | ifneq ($(strip $(GTK2_CFLAGS)),) 21 | TARGETS+=$(OUT_GTK2) 22 | endif 23 | ifneq ($(strip $(GTK3_CFLAGS)),) 24 | TARGETS+=$(OUT_GTK3) 25 | endif 26 | 27 | ifndef TARGETS 28 | $(error Need at least on of GTK2 or GTK3 to build the plugin) 29 | endif 30 | $(info Building $(TARGETS)) 31 | 32 | .PHONY: install all clean 33 | all: $(TARGETS) 34 | 35 | install: all 36 | @if [ -f $(OUT_GTK2) ]; then \ 37 | echo Installing $(PREFIX)/lib/deadbeef/$(OUT_GTK2); \ 38 | install -D $(OUT_GTK2) $(PREFIX)/lib/deadbeef/$(OUT_GTK2); \ 39 | fi 40 | @if [ -f $(OUT_GTK3) ]; then \ 41 | echo Installing $(PREFIX)/lib/deadbeef/$(OUT_GTK3) \ 42 | install -D $(OUT_GTK3) $(PREFIX)/lib/deadbeef/$(OUT_GTK3); \ 43 | fi 44 | 45 | $(OUT_GTK2): $(OBJECTS_GTK2) 46 | $(CC) $(OBJECTS_GTK2) $(LDFLAGS) $(GTK2_LIBS) -o $@ 47 | 48 | $(OUT_GTK3): $(OBJECTS_GTK3) 49 | $(CC) $(OBJECTS_GTK3) $(LDFLAGS) $(GTK3_LIBS) -o $@ 50 | 51 | %_gtk2.o: %.c 52 | $(CC) $(CFLAGS) $(GTK2_CFLAGS) $< -c -o $@ 53 | 54 | %_gtk3.o: %.c 55 | $(CC) $(CFLAGS) $(GTK3_CFLAGS) $< -c -o $@ 56 | 57 | clean: 58 | rm -f $(OBJECTS_GTK2) $(OBJECTS_GTK3) $(OUT_GTK2) $(OUT_GTK3) 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | db-vk 2 | ===== 3 | DeadBeef plugin for listening musing from vkontakte.com 4 | 5 | **The plugin is discontinued and doesn't work any more due to VK music API switch off https://vk.com/dev/audio_api** 6 | 7 | For those speaking only Russian 8 | -------- 9 | За помощью можно обращаться в группу на ВК 10 | 11 | **Плагин не разивается и больше не работает из-за отключения VK API для музыки https://vk.com/dev/audio_api** 12 | 13 | Features 14 | -------- 15 | * Retrieve 'My Music' contents 16 | * Retrieve 'Suggested Music' contents 17 | * Get music from group or users by id 18 | * Search VK.com for music 19 | * Removes duplicates in search results 20 | * Narrows search to specific phrase (in contrast to default behaviour which matches any single word from search query) 21 | * Allow searching in artist name or track title only 22 | * Copy track(s) URL to clipboard (for later download or whatever) 23 | 24 | Track(s) can be added to current playlist by double click or with popup menu. 25 | That's it for now. 26 | 27 | Installation 28 | ------------ 29 | ### Dependencies 30 | * gtk+ (2 or 3 - should correspond to GTK version your Deadbeef is built with) 31 | * json-glib (git version requires jansson instead) 32 | * curl 33 | * cmake 34 | * Deadbeef dev files (`deadbeef.h` and `gtkui.h`) 35 | 36 | For example on Ubuntu the below installs required packages 37 | 38 | sudo apt-get install libgtk2.0-dev libgtk-3-dev \ 39 | libcurl4-gnutls-dev libjson-glib-dev cmake 40 | 41 | ### Building 42 | Build it with 43 | 44 | cmake . 45 | make 46 | and copy `vkontakte_gtk*.so` to `~/.local/lib/deadbeef` like this: 47 | 48 | mkdir -p ~/.local/lib/deadbeef 49 | cp vkontakte_gtk*.so ~/.local/lib/deadbeef/ 50 | Restart Deadbeeef player for it to load the plugin, now check out `File` menu 51 | 52 | Packages 53 | -------- 54 | [Arch Linux (AUR)] (https://aur.archlinux.org/packages/deadbeef-plugin-vk/) 55 | [Ubuntu 12.10 (GTK2 only build)] (https://github.com/scorpp/db-vk/releases). (Reported to work fine on Debian stable as well.) Package installs to /opt/deadbeef/ prefix as official Deadbeef package does. If you have a thirdparty package installed you may need to copy\symlink .so's to ~/.local/lib/deadbeef/ or /usr/lib/deadbeef/ 56 | [Gentoo] (https://github.com/megabaks/stuff/tree/master/media-plugins/deadbeef-vk) (appreciations to @megabaks) 57 | 58 | Contacts 59 | -------- 60 | * Found a bug or have a problem? Raise an issue here! (This method is appreciated) 61 | * Don't have a github account? Comment on VK group 62 | * Me on vk.com 63 | * Me on Google+ 64 | * Email & GTalk: keryascorpio [at] gmail.com 65 | 66 | I'll be glad to any kind of feedback from you! :) 67 | -------------------------------------------------------------------------------- /src/common-defs.h: -------------------------------------------------------------------------------- 1 | /* 2 | * common-defs.h 3 | * 4 | * Created on: Dec 9, 2012 5 | * Author: scorp 6 | */ 7 | 8 | #ifndef COMMON_DEFS_H_ 9 | #define COMMON_DEFS_H_ 10 | 11 | #include 12 | 13 | G_BEGIN_DECLS 14 | 15 | #define trace(...) { g_fprintf(stderr, __VA_ARGS__); } 16 | 17 | 18 | typedef void (*DB_thread_func_t)(void *ctx); 19 | 20 | G_END_DECLS 21 | 22 | #endif /* COMMON_DEFS_H_ */ 23 | -------------------------------------------------------------------------------- /src/core.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "core.h" 10 | #include "common-defs.h" 11 | #include "vk-api.h" 12 | #include "ui.h" 13 | #include "util.h" 14 | 15 | 16 | // URL formatting strings 17 | #define MAX_URL_LEN 300 18 | #define VK_AUDIO_GET VK_API_METHOD_AUDIO_GET "?access_token=%s" 19 | #define VK_AUDIO_GET_BY_OWNER VK_API_METHOD_AUDIO_GET "?access_token=%s&owner_id=%ld" 20 | #define VK_AUDIO_GET_BY_ID VK_API_METHOD_AUDIO_GET_BY_ID "?access_token=%s&audios=%d_%d" 21 | #define VK_AUDIO_SEARCH VK_API_METHOD_AUDIO_SEARCH "?access_token=%s&count=%d&offset=%d&q=%s" 22 | #define VK_AUDIO_GET_RECOMMENDATIONS VK_API_METHOD_AUDIO_GET_RECOMMENDATIONS "?access_token=%s&count=%d&offset=%d" 23 | #define VK_UTILS_RESOLVE_SCREEN_NAME VK_API_METHOD_UTILS_RESOLVE_SCREEN_NAME "?access_token=%s&screen_name=%s" 24 | 25 | // Max length for VFS URL to VK track 26 | #define VK_VFS_URL_LEN 30 27 | 28 | #define vk_send_audio_request_and_parse_response_va(query, url_format, ...) { \ 29 | gchar formatted_url[MAX_URL_LEN]; \ 30 | sprintf (formatted_url, url_format, __VA_ARGS__); \ 31 | vk_send_audio_request_and_parse_response (query, formatted_url); \ 32 | } 33 | 34 | 35 | static DB_functions_t *deadbeef; 36 | /** Used by ui.c to detect design mode */ 37 | ddb_gtkui_t *gtkui_plugin; 38 | static VkAuthData *vk_auth_data = NULL; 39 | /** When set to TRUE will replace HTTPS track URLs to plain HTTP ones. */ 40 | static gboolean tracks_force_http = FALSE; 41 | static intptr_t http_tid; // thread for communication 42 | 43 | typedef struct { 44 | const gchar *query; 45 | glong id; // user or group id 46 | GtkListStore *store; 47 | } SearchQuery; 48 | 49 | 50 | static int 51 | vk_vfs_format_track_url (char *url, 52 | int aid, 53 | int owner_id) { 54 | return sprintf (url, "vk://%d_%d", owner_id, aid); 55 | } 56 | 57 | /** 58 | * Simple deduplication. 59 | * @return TRUE if given track already exists, FALSE otherwise. 60 | */ 61 | static gboolean 62 | vk_tree_model_has_track (GtkTreeModel *treemodel, VkAudioTrack *track) { 63 | // if no need to filter duplicates return FALSE immediately 64 | if (!vk_search_opts.filter_duplicates) { 65 | return FALSE; 66 | } 67 | 68 | gboolean has_track; 69 | GtkTreeIter iter; 70 | gboolean valid; 71 | gchar *track_artist_casefolded; 72 | gchar *track_title_casefolded; 73 | 74 | has_track = FALSE; 75 | 76 | track_artist_casefolded = g_utf8_casefold (track->artist, -1); 77 | track_title_casefolded = g_utf8_casefold (track->title, -1); 78 | 79 | valid = gtk_tree_model_get_iter_first (treemodel, &iter); 80 | while (valid && !has_track) { 81 | gchar *model_artist; 82 | gchar *model_title; 83 | gchar *model_artist_casefolded; 84 | gchar *model_title_casefolded; 85 | gint model_duration; 86 | 87 | gtk_tree_model_get (treemodel, &iter, 88 | ARTIST_COLUMN, &model_artist, 89 | TITLE_COLUMN, &model_title, 90 | DURATION_COLUMN, &model_duration, 91 | -1); 92 | model_artist_casefolded = g_utf8_casefold (model_artist, -1); 93 | model_title_casefolded = g_utf8_casefold (model_title, -1); 94 | 95 | if (0 == g_utf8_collate (model_artist_casefolded, track_artist_casefolded) 96 | && 0 == g_utf8_collate (model_title_casefolded, track_title_casefolded)) { 97 | has_track = TRUE; 98 | trace ("Duplicate %s - %s, duration existing %d vs %d\n", track->artist, track->title, 99 | model_duration, track->duration); 100 | } 101 | 102 | g_free (model_artist); 103 | g_free (model_title); 104 | g_free (model_artist_casefolded); 105 | g_free (model_title_casefolded); 106 | 107 | valid = gtk_tree_model_iter_next (treemodel, &iter); 108 | } 109 | 110 | g_free (track_artist_casefolded); 111 | g_free (track_title_casefolded); 112 | 113 | return has_track; 114 | } 115 | 116 | /** 117 | * Apply additional filters. 118 | * @return TRUE if track matches additional filters, FALSE otherwise. 119 | */ 120 | static gboolean 121 | vk_search_filter_matches (const gchar *search_query, VkAudioTrack *track) { 122 | // 'My music' doesn't use search query, don't filter it 123 | if (!vk_search_opts.search_whole_phrase 124 | || search_query == NULL) { 125 | return TRUE; 126 | } 127 | 128 | gchar *query_casefolded = g_utf8_casefold (search_query, -1); 129 | gchar *title_casefolded = g_utf8_casefold (track->title, -1); 130 | gchar *artist_casefolded = g_utf8_casefold (track->artist, -1); 131 | 132 | gboolean artist_matches = strstr (artist_casefolded, query_casefolded) != 0; 133 | gboolean title_matches = strstr (title_casefolded, query_casefolded) != 0; 134 | 135 | g_free (artist_casefolded); 136 | g_free (title_casefolded); 137 | g_free (query_casefolded); 138 | 139 | switch (vk_search_opts.search_target) { 140 | case VK_TARGET_ANY_FIELD: 141 | return artist_matches || title_matches; 142 | case VK_TARGET_ARTIST_FIELD: 143 | return artist_matches; 144 | case VK_TARGET_TITLE_FIELD: 145 | return title_matches; 146 | default: 147 | trace ("WARN: unexpected VkSearchTraget value: %d\n", vk_search_opts.search_target); 148 | return TRUE; 149 | } 150 | } 151 | 152 | static void 153 | parse_audio_track_callback(VkAudioTrack *track, size_t index, SearchQuery *query) { 154 | GtkTreeIter iter; 155 | 156 | if (vk_search_filter_matches (query->query, track) 157 | && !vk_tree_model_has_track (GTK_TREE_MODEL (query->store), track)) { 158 | 159 | char duration_formatted[10]; 160 | sprintf (duration_formatted, "%d:%02d", track->duration / 60, track->duration % 60); 161 | 162 | // write to list store 163 | gtk_list_store_append(GTK_LIST_STORE (query->store), &iter); 164 | gtk_list_store_set (GTK_LIST_STORE (query->store), &iter, 165 | ARTIST_COLUMN, track->artist, 166 | TITLE_COLUMN, track->title, 167 | DURATION_COLUMN, track->duration, 168 | DURATION_FORMATTED_COLUMN, duration_formatted, 169 | URL_COLUMN, track->url, 170 | AID_COLUMN, track->aid, 171 | OWNER_ID_COLUMN, track->owner_id, 172 | -1); 173 | } 174 | } 175 | 176 | static void 177 | parse_audio_resp (SearchQuery *query, const gchar *resp_str) { 178 | GError *error; 179 | 180 | gdk_threads_enter (); 181 | if (!vk_audio_response_parse (resp_str, (VkAudioTrackCallback) parse_audio_track_callback, query, &error)) { 182 | show_message(GTK_MESSAGE_ERROR, error->message); 183 | g_error_free (error); 184 | } 185 | gdk_threads_leave (); 186 | } 187 | 188 | void 189 | vk_add_tracks_from_tree_model_to_playlist (GtkTreeModel *treemodel, GList *gtk_tree_path_list, const char *plt_name) { 190 | ddb_playlist_t *plt; 191 | 192 | if (plt_name) { 193 | int idx = deadbeef->plt_add (deadbeef->plt_get_count (), plt_name); 194 | deadbeef->plt_set_curr_idx (idx); 195 | plt = deadbeef->plt_get_for_idx (idx); 196 | } else { 197 | plt = deadbeef->plt_get_curr (); 198 | } 199 | 200 | if (!deadbeef->plt_add_files_begin (plt, 0)) { 201 | DB_playItem_t *last = deadbeef->plt_get_last (plt, 0); 202 | 203 | gtk_tree_path_list = g_list_last (gtk_tree_path_list); 204 | while (gtk_tree_path_list) { 205 | GtkTreeIter iter; 206 | gchar *artist; 207 | gchar *title; 208 | int duration; 209 | int aid; 210 | int owner_id; 211 | char url[VK_VFS_URL_LEN]; 212 | 213 | if (gtk_tree_model_get_iter(treemodel, &iter, (GtkTreePath *) gtk_tree_path_list->data)) { 214 | DB_playItem_t *pt; 215 | int pabort = 0; 216 | 217 | gtk_tree_model_get (treemodel, &iter, 218 | ARTIST_COLUMN, &artist, 219 | TITLE_COLUMN, &title, 220 | DURATION_COLUMN, &duration, 221 | AID_COLUMN, &aid, 222 | OWNER_ID_COLUMN, &owner_id, 223 | -1); 224 | vk_vfs_format_track_url (url, aid, owner_id); 225 | 226 | pt = deadbeef->plt_insert_file2 (0, plt, last, url, &pabort, NULL, NULL); 227 | deadbeef->pl_add_meta (pt, "artist", artist); 228 | deadbeef->pl_add_meta (pt, "title", title); 229 | deadbeef->plt_set_item_duration (plt, pt, duration); 230 | 231 | g_free (artist); 232 | g_free (title); 233 | } 234 | 235 | gtk_tree_path_list = g_list_previous (gtk_tree_path_list); 236 | } 237 | 238 | if (last) { 239 | deadbeef->pl_item_unref (last); 240 | } 241 | } 242 | 243 | deadbeef->plt_add_files_end (plt, 0); 244 | deadbeef->plt_save_config (plt); 245 | deadbeef->plt_unref (plt); 246 | } 247 | 248 | static void 249 | vk_send_audio_request_and_parse_response (SearchQuery *query, const gchar *url) { 250 | GError *error; 251 | gchar *resp_str; 252 | 253 | resp_str = http_get_string (url, &error); 254 | if (NULL == resp_str) { 255 | trace ("VK error: %s\n", error->message); 256 | g_error_free (error); 257 | } else { 258 | parse_audio_resp (query, resp_str); 259 | g_free (resp_str); 260 | } 261 | } 262 | 263 | static void 264 | vk_search_audio_thread_func (SearchQuery *query) { 265 | CURL *curl; 266 | gint rows_added; 267 | gint iteration = 0; 268 | 269 | curl = curl_easy_init (); 270 | 271 | char *escaped_search_str = curl_easy_escape (curl, query->query, 0); 272 | 273 | do { 274 | rows_added = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (query->store), NULL); 275 | vk_send_audio_request_and_parse_response_va (query, VK_AUDIO_SEARCH, 276 | vk_auth_data->access_token, 277 | VK_AUDIO_MAX_TRACKS, 278 | VK_AUDIO_MAX_TRACKS * iteration, 279 | escaped_search_str); 280 | rows_added = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (query->store), NULL) - rows_added; 281 | } while (++iteration < 10 && rows_added > 0); 282 | 283 | trace ("INFO: Did %d iterations and stopped discovering new tracks\n", iteration); 284 | 285 | g_free (escaped_search_str); 286 | curl_easy_cleanup (curl); 287 | g_free ((gchar *) query->query); 288 | g_free (query); 289 | http_tid = 0; 290 | } 291 | 292 | static void 293 | vk_get_my_music_thread_func (void *ctx) { 294 | SearchQuery query; 295 | 296 | query.query = NULL, 297 | query.store = GTK_LIST_STORE (ctx); 298 | 299 | vk_send_audio_request_and_parse_response_va (&query, VK_AUDIO_GET, vk_auth_data->access_token); 300 | 301 | http_tid = 0; 302 | } 303 | 304 | static void 305 | vk_get_recommended_music_thread_func(void *ctx) { 306 | SearchQuery query; 307 | gint rows_added; 308 | gint iteration = 0; 309 | query.query = NULL, 310 | query.store = GTK_LIST_STORE (ctx); 311 | do { 312 | rows_added = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (query.store), NULL); 313 | vk_send_audio_request_and_parse_response_va (&query, VK_AUDIO_GET_RECOMMENDATIONS, 314 | vk_auth_data->access_token, 315 | VK_AUDIO_MAX_TRACKS, 316 | VK_AUDIO_MAX_TRACKS * iteration); 317 | rows_added = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (query.store), NULL) - rows_added; 318 | } while (++iteration < 10 && rows_added > 0); 319 | http_tid = 0; 320 | } 321 | 322 | static void 323 | vk_get_by_owner_music_thread_func (SearchQuery *query) { 324 | CURL *curl; 325 | curl = curl_easy_init (); 326 | 327 | vk_send_audio_request_and_parse_response_va (query, VK_AUDIO_GET_BY_OWNER, vk_auth_data->access_token, query->id); 328 | 329 | curl_easy_cleanup (curl); 330 | g_free (query); 331 | http_tid = 0; 332 | } 333 | 334 | /** 335 | * Try to detect if search_text is a known vk.com link and configure `query` correspondingly. 336 | * 337 | * There are three possible outcomes - link to a user profile, link to a group or (fallback option) just 338 | * regular query text. 339 | */ 340 | static void 341 | vk_detect_search_target(const gchar *search_text, SearchQuery *query) { 342 | const gchar *remains = NULL; 343 | gchar url[MAX_URL_LEN]; 344 | gchar *resp_str; 345 | GError *error = NULL; 346 | 347 | query->query = NULL; 348 | 349 | // try to find of know vk.com prefixes in search string 350 | for (gint i = 0; remains == NULL && VK_PUBLIC_SITE_PREFIXES[i] != NULL; i++) { 351 | remains = strip_prefix (search_text, VK_PUBLIC_SITE_PREFIXES[i]); 352 | } 353 | if (remains == NULL) { 354 | // not a vk.com url, fallback to plain search 355 | query->query = g_strdup (search_text); 356 | return; 357 | } 358 | 359 | // ok, it is an vk.com url indeed! an object may be identified with ID or a string alias 360 | if (g_str_has_prefix (remains, "id")) { 361 | query->id = strtol (remains + 2, NULL, 10); 362 | trace("Searched URL containing user id=%ld\n", query->id); 363 | return; 364 | 365 | } else if (g_str_has_prefix (remains, "club")) { 366 | query->id = -1 * strtol (remains + 4, NULL, 10); 367 | trace("Searched URL containing group id=%ld\n", query->id); 368 | return; 369 | } 370 | 371 | // else this is an alias 372 | trace("Searched URL containing name alias=%s\n", remains); 373 | sprintf (url, VK_UTILS_RESOLVE_SCREEN_NAME, vk_auth_data->access_token, remains); 374 | resp_str = http_get_string (url, &error); 375 | if (NULL == resp_str) { 376 | trace ("VK error: %s\n", error->message); 377 | query->query = g_strdup (search_text); 378 | g_error_free (error); 379 | return; 380 | 381 | } else { 382 | glong id = vk_utils_resolve_screen_name_parse (resp_str, &error); 383 | if (!id) { 384 | trace("Unknown object type under alias %s\n", remains); 385 | query->query = g_strdup (search_text); 386 | g_error_free (error); 387 | } else { 388 | query->id = id; 389 | } 390 | g_free (resp_str); 391 | } 392 | } 393 | 394 | static void 395 | vk_kill_http_thread () { 396 | if (http_tid) { 397 | trace("Killing http thread\n"); 398 | deadbeef->thread_detach (http_tid); 399 | 400 | http_tid = 0; 401 | } 402 | } 403 | 404 | void 405 | vk_search_music (const gchar *query_text, GtkListStore *liststore) { 406 | SearchQuery *query; 407 | 408 | vk_kill_http_thread (); 409 | trace("== Searching for %s\n", query_text); 410 | 411 | query = g_malloc (sizeof(SearchQuery)); 412 | query->store = liststore; 413 | // TODO the below func performs http rq, need to call it in separate thread 414 | vk_detect_search_target (query_text, query); 415 | 416 | if (query->query == NULL) { 417 | http_tid = deadbeef->thread_start ((DB_thread_func_t) vk_get_by_owner_music_thread_func, query); 418 | } else { 419 | http_tid = deadbeef->thread_start ((DB_thread_func_t) vk_search_audio_thread_func, query); 420 | } 421 | } 422 | 423 | void 424 | vk_get_my_music (GtkListStore *liststore) { 425 | vk_kill_http_thread (); 426 | trace("== Getting my music, uid=%ld\n", vk_auth_data->user_id); 427 | 428 | http_tid = deadbeef->thread_start (vk_get_my_music_thread_func, liststore); 429 | } 430 | 431 | void 432 | vk_get_recommended_music (GtkListStore *liststore) { 433 | vk_kill_http_thread (); 434 | trace("== Getting my music, uid=%ld\n", vk_auth_data->user_id); 435 | 436 | http_tid = deadbeef->thread_start (vk_get_recommended_music_thread_func, liststore); 437 | } 438 | 439 | static DB_FILE * 440 | deadbeef_fopen (const gchar *url) { 441 | trace ("vk fopen by deadbeef: %s\n", url); 442 | return deadbeef->fopen (url); 443 | } 444 | 445 | static void 446 | vk_vfs_store_track (VkAudioTrack *track, int index, DB_FILE **f) { 447 | if (index == 0) { 448 | // TODO ensure URL is of supported scheme 449 | if (tracks_force_http 450 | && g_str_has_prefix (track->url, "https://")) { 451 | gchar *http_url = repl_str (track->url, "https://", "http://"); 452 | *f = deadbeef_fopen (http_url); 453 | g_free (http_url); 454 | } else { 455 | *f = deadbeef_fopen (track->url); 456 | } 457 | } 458 | } 459 | 460 | DB_FILE * 461 | vk_vfs_open (const gchar* fname) { 462 | int owner; 463 | int aid; 464 | GError *error; 465 | DB_FILE *f = 0; 466 | char *audio_resp; 467 | char get_audio_url[MAX_URL_LEN]; 468 | 469 | if (!vk_auth_data || !vk_auth_data->access_token) { 470 | trace ("Not authenticated? Visit VK.com\n"); 471 | return 0; 472 | } 473 | 474 | // retrieve audio URL 475 | sscanf (fname, "vk://%d_%d", &owner, &aid); 476 | sprintf (get_audio_url, VK_AUDIO_GET_BY_ID, vk_auth_data->access_token, owner, aid); 477 | audio_resp = http_get_string (get_audio_url, &error); 478 | 479 | if (audio_resp) { 480 | // got URL, delegate the rest to other plugin 481 | vk_audio_response_parse (audio_resp, 482 | (VkAudioTrackCallback) vk_vfs_store_track, 483 | &f, 484 | &error); 485 | g_free (audio_resp); 486 | 487 | } else { 488 | trace ("Cannot get URL for VK audio %d_%d\n", owner, aid); 489 | g_error_free (error); 490 | } 491 | 492 | return f; 493 | } 494 | 495 | gboolean 496 | vk_action_gtk (void *data) { 497 | if (vk_auth_data == NULL) { 498 | // not authenticated, show warning and that's it 499 | trace ("VK - not authenticated\n") 500 | gdk_threads_enter (); 501 | show_message (GTK_MESSAGE_WARNING, 502 | "To be able to use VKontakte plugin you need to provide your\n" 503 | "authentication details. Please visit plugin configuration.\n" 504 | "Then you will be able to add tracks from VK.com"); 505 | gdk_threads_leave (); 506 | return FALSE; 507 | } 508 | 509 | gtk_widget_show (vk_create_browser_dialogue ()); 510 | return FALSE; 511 | } 512 | 513 | ddb_gtkui_widget_t * 514 | w_vkbrowser_create () { 515 | if (vk_auth_data == NULL) { 516 | // not authenticated, show warning and that's it 517 | // TODO 518 | } 519 | 520 | ddb_gtkui_widget_t *w = malloc (sizeof (ddb_gtkui_widget_t)); 521 | memset (w, 0, sizeof (ddb_gtkui_widget_t)); 522 | vk_setup_browser_widget (w); 523 | return w; 524 | } 525 | 526 | void 527 | vk_config_changed () { 528 | // restore auth url if it was occasionally changed 529 | deadbeef->conf_set_str (CONF_VK_AUTH_URL, VK_AUTH_URL); 530 | 531 | // read VK auth data 532 | deadbeef->conf_lock (); 533 | const gchar *auth_data_str = deadbeef->conf_get_str_fast (CONF_VK_AUTH_DATA, NULL); 534 | // old version of authentication page used single quotes instead of double. so silly! 535 | if (auth_data_str != NULL) { 536 | auth_data_str = repl_str (auth_data_str, "'", "\""); 537 | } 538 | 539 | tracks_force_http = deadbeef->conf_get_int (CONF_TRACKS_FORCE_HTTP, 0); 540 | deadbeef->conf_unlock (); 541 | 542 | vk_auth_data_free (vk_auth_data); 543 | vk_auth_data = vk_auth_data_parse (auth_data_str); 544 | 545 | g_free ((gpointer) auth_data_str); 546 | } 547 | 548 | void 549 | vk_set_config_var (const char *key, GValue *value) { 550 | if (G_VALUE_HOLDS_INT (value)) { 551 | deadbeef->conf_set_int (key, g_value_get_int (value)); 552 | } else if (G_VALUE_HOLDS_INT64 (value)) { 553 | deadbeef->conf_set_int64 (key, (int64_t) g_value_get_int64 (value)); 554 | } else if (G_VALUE_HOLDS_FLOAT (value)) { 555 | deadbeef->conf_set_float (key, g_value_get_float (value)); 556 | } else if (G_VALUE_HOLDS_STRING (value)) { 557 | deadbeef->conf_set_str (key, g_value_get_string (value)); 558 | } else if (G_VALUE_HOLDS_BOOLEAN (value)) { 559 | deadbeef->conf_set_int (key, g_value_get_boolean (value) ? 1 : 0); 560 | } else { 561 | trace ("WARN unsupported GType to vk_set_config_var: %s\n", G_VALUE_TYPE_NAME (value)); 562 | } 563 | } 564 | 565 | void 566 | vk_initialise (DB_functions_t *deadbeef_instance, ddb_gtkui_t *gtkui_instance) { 567 | deadbeef = deadbeef_instance; 568 | gtkui_plugin = gtkui_instance; 569 | // set default UI options 570 | vk_search_opts.filter_duplicates = (1 == deadbeef->conf_get_int (CONF_VK_UI_DEDUP, 1)); 571 | vk_search_opts.search_whole_phrase = (1 == deadbeef->conf_get_int (CONF_VK_UI_WHOLE_PHRASE, 1)); 572 | vk_search_opts.search_target = (VkSearchTarget) deadbeef->conf_get_int (CONF_VK_UI_TARGET, VK_TARGET_ANY_FIELD); 573 | } 574 | 575 | void 576 | vk_perform_cleanup () { 577 | vk_kill_http_thread (); 578 | vk_auth_data_free (vk_auth_data); 579 | } 580 | -------------------------------------------------------------------------------- /src/core.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef DB_VK_CORE_H 3 | #define DB_VK_CORE_H 4 | 5 | #include 6 | #include 7 | 8 | 9 | #define VK_AUTH_APP_ID "3035566" 10 | #define VK_AUTH_REDIR_URL "http://scorpp.github.io/db-vk/vk-id.html" 11 | #define VK_AUTH_URL "http://oauth.vkontakte.ru/authorize" \ 12 | "?client_id=" VK_AUTH_APP_ID \ 13 | "&scope=audio,friends,groups,offline" \ 14 | "&redirect_uri=" VK_AUTH_REDIR_URL \ 15 | "&response_type=token" 16 | 17 | // deadbeef config keys 18 | #define CONF_VK_AUTH_URL "vk.auth.url" 19 | #define CONF_VK_AUTH_DATA "vk.auth.data" 20 | #define CONF_TRACKS_FORCE_HTTP "vk.tracks.force.http" 21 | 22 | 23 | /// Plugin lifecycle 24 | 25 | gboolean vk_action_gtk (void *data); 26 | 27 | ddb_gtkui_widget_t *w_vkbrowser_create (); 28 | 29 | void vk_config_changed (); 30 | 31 | DB_FILE *vk_vfs_open (const gchar *fname); 32 | 33 | void vk_initialise (DB_functions_t *deadbeef_instance, ddb_gtkui_t *gtkui_plugin); 34 | 35 | void vk_perform_cleanup (); 36 | 37 | /// UI backend 38 | 39 | void vk_add_tracks_from_tree_model_to_playlist (GtkTreeModel *treemodel, 40 | GList *gtk_tree_path_list, 41 | const char *plt_name); 42 | 43 | void vk_search_music (const gchar *query_text, GtkListStore *liststore); 44 | 45 | void vk_get_my_music (GtkListStore *liststore); 46 | 47 | void vk_get_recommended_music (GtkListStore *liststore); 48 | 49 | void vk_set_config_var (const char *key, GValue *value); 50 | 51 | #endif //DB_VK_CORE_H 52 | -------------------------------------------------------------------------------- /src/gtk_compat.h: -------------------------------------------------------------------------------- 1 | #if !GTK_CHECK_VERSION(2, 14, 0) 2 | #define gtk_dialog_get_content_area(dialog) (dialog->vbox) 3 | #endif 4 | 5 | #if !GTK_CHECK_VERSION(2, 24, 0) 6 | #define GTK_COMBO_BOX_TEXT(o) GTK_COMBO_BOX (o) 7 | #define gtk_combo_box_text_new() gtk_combo_box_new_text () 8 | #define gtk_combo_box_text_append_text(widget, text) gtk_combo_box_append_text (widget, text) 9 | #define gtk_combo_box_text_prepend_text(widget, text) gtk_combo_box_prepend_text (widget, text) 10 | #define gtk_combo_box_text_remove(widget, text) gtk_combo_box_remove_text (widget, pos) 11 | #endif 12 | 13 | #if !GTK_CHECK_VERSION(3, 0, 0) 14 | #define gtk_box_new(orientation, spacing) (orientation == GTK_ORIENTATION_HORIZONTAL \ 15 | ? gtk_hbox_new (FALSE, spacing)\ 16 | : gtk_vbox_new (FALSE, spacing)) 17 | #endif 18 | 19 | 20 | 21 | #if !GLIB_CHECK_VERSION(2, 30, 0) 22 | #define G_VALUE_INIT { 0, { { 0 } } } 23 | #endif 24 | -------------------------------------------------------------------------------- /src/ui.c: -------------------------------------------------------------------------------- 1 | /* 2 | * ui.c 3 | * 4 | * Created on: Dec 9, 2012 5 | * Author: scorp 6 | */ 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "ui.h" 13 | #include "core.h" 14 | #include "common-defs.h" 15 | #include "gtk_compat.h" 16 | 17 | 18 | static const gchar *last_search_query = NULL; 19 | extern ddb_gtkui_t *gtkui_plugin; 20 | 21 | 22 | /** 23 | * Handler for various search-affecting controls that would trigger search again if needed. 24 | */ 25 | static void 26 | maybe_do_search_again (GtkWidget *widget, gpointer data) { 27 | if (last_search_query == NULL) { 28 | return; 29 | } 30 | 31 | const gchar *current_query = gtk_entry_get_text (GTK_ENTRY (data)); 32 | 33 | // don't care about encodings - if it's the same string there would be zero 34 | if (strcmp (last_search_query, current_query) == 0) { 35 | // if search query wasn't changed, emit search to refresh results 36 | g_signal_emit_by_name (data, "activate"); 37 | } 38 | } 39 | 40 | static void 41 | save_active_property_value_to_config (GtkWidget *widget, gpointer data) { 42 | GValue value = G_VALUE_INIT; 43 | 44 | if (GTK_IS_COMBO_BOX (widget)) { 45 | g_value_init (&value, G_TYPE_INT); 46 | } else if (GTK_IS_CHECK_BUTTON (widget)) { 47 | g_value_init (&value, G_TYPE_BOOLEAN); 48 | } else { 49 | trace ("FATAL: %s unsupported widget type\n", __FUNCTION__); 50 | } 51 | 52 | g_object_get_property (G_OBJECT (widget), "active", &value); 53 | vk_set_config_var ((const gchar *) data, &value); 54 | } 55 | 56 | static void 57 | add_to_playlist (GtkTreeView *tree_view, const char *playlist) { 58 | GtkTreeSelection *selection; 59 | GtkTreeModel *treemodel; 60 | GList *selected_rows; 61 | 62 | selection = gtk_tree_view_get_selection (tree_view); 63 | selected_rows = gtk_tree_selection_get_selected_rows (selection, &treemodel); 64 | 65 | vk_add_tracks_from_tree_model_to_playlist (treemodel, selected_rows, playlist); 66 | 67 | g_list_free (selected_rows); 68 | } 69 | 70 | static void 71 | on_search_results_row_activate (GtkTreeView *tree_view, 72 | GtkTreePath *path, 73 | GtkTreeViewColumn *column, 74 | gpointer user_data) { 75 | add_to_playlist (tree_view, NULL); 76 | } 77 | 78 | static void 79 | on_menu_item_add_to_playlist (GtkWidget *menu_item, GtkTreeView *tree_view) { 80 | add_to_playlist (tree_view, NULL); 81 | } 82 | 83 | static void 84 | on_menu_item_add_to_new_playlist (GtkWidget *menu_item, GtkTreeView *tree_view) { 85 | add_to_playlist (tree_view, last_search_query); 86 | } 87 | 88 | static void 89 | on_menu_item_copy_url (GtkWidget *menu_item, GtkTreeView *treeview) { 90 | GtkTreeSelection *selection; 91 | GtkTreeModel *treemodel; 92 | GtkTreeIter iter; 93 | GList *selected_rows, *i; 94 | GString *urls_buf; 95 | 96 | urls_buf = g_string_sized_new(500); 97 | 98 | selection = gtk_tree_view_get_selection (treeview); 99 | selected_rows = gtk_tree_selection_get_selected_rows (selection, &treemodel); 100 | 101 | i = g_list_first (selected_rows); 102 | while (i) { 103 | gchar *track_url; 104 | 105 | gtk_tree_model_get_iter (treemodel, &iter, (GtkTreePath *) i->data); 106 | gtk_tree_model_get (treemodel, &iter, 107 | URL_COLUMN, &track_url, 108 | -1); 109 | g_string_append (urls_buf, track_url); 110 | g_string_append (urls_buf, "\n"); 111 | g_free (track_url); 112 | 113 | i = g_list_next (i); 114 | } 115 | 116 | gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD), urls_buf->str, urls_buf->len); 117 | 118 | g_list_free (selected_rows); 119 | g_string_free(urls_buf, TRUE); 120 | } 121 | 122 | static void 123 | show_popup_menu (GtkTreeView *treeview, GdkEventButton *event) { 124 | GtkTreeSelection *selection; 125 | GtkWidget *menu, *item; 126 | char label_buf[200]; 127 | 128 | selection = gtk_tree_view_get_selection (treeview); 129 | if (!gtk_tree_selection_count_selected_rows (selection)) { 130 | // don't show menu on empty tree view 131 | return; 132 | } 133 | 134 | menu = gtk_menu_new (); 135 | 136 | sprintf(label_buf, "Add to playlist '%s'", last_search_query); 137 | item = gtk_menu_item_new_with_label (label_buf); 138 | g_signal_connect (item, "activate", G_CALLBACK (on_menu_item_add_to_new_playlist), treeview); 139 | gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); 140 | 141 | item = gtk_menu_item_new_with_label ("Add to current playlist"); 142 | g_signal_connect (item, "activate", G_CALLBACK (on_menu_item_add_to_playlist), treeview); 143 | gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); 144 | 145 | item = gtk_menu_item_new_with_label ("Copy URL(s)"); 146 | g_signal_connect (item, "activate", G_CALLBACK (on_menu_item_copy_url), treeview); 147 | gtk_menu_shell_append (GTK_MENU_SHELL(menu), item); 148 | 149 | gtk_widget_show_all (menu); 150 | gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 0, 151 | gdk_event_get_time ((GdkEvent *) event)); 152 | } 153 | 154 | static gboolean 155 | on_search_results_button_press (GtkTreeView *treeview, GdkEventButton *event, gpointer userdata) { 156 | if (!gtkui_plugin->w_get_design_mode () 157 | && event->type == GDK_BUTTON_PRESS && event->button == 3) { 158 | GtkTreeSelection *selection; 159 | 160 | selection = gtk_tree_view_get_selection (treeview); 161 | if (gtk_tree_selection_count_selected_rows (selection) <= 1) { 162 | GtkTreePath *path; 163 | if (gtk_tree_view_get_path_at_pos (treeview, event->x, event->y, &path, NULL, NULL, NULL )) { 164 | gtk_tree_selection_unselect_all(selection); 165 | gtk_tree_selection_select_path (selection, path); 166 | gtk_tree_path_free (path); 167 | } 168 | } 169 | 170 | show_popup_menu (treeview, event); 171 | return TRUE; 172 | } 173 | return FALSE; 174 | } 175 | 176 | static gboolean 177 | on_search_results_popup_menu (GtkTreeView *treeview, gpointer userdata) { 178 | show_popup_menu(treeview, NULL); 179 | return TRUE; 180 | } 181 | 182 | static void 183 | on_search (GtkWidget *widget, gpointer data) { 184 | gtk_widget_set_sensitive (widget, FALSE); 185 | 186 | const gchar *query_text = gtk_entry_get_text (GTK_ENTRY (widget)); 187 | 188 | // refresh last search query 189 | if (last_search_query != NULL) { 190 | g_free ((gchar*) last_search_query); 191 | } 192 | last_search_query = g_strdup (query_text); 193 | 194 | gtk_list_store_clear (GTK_LIST_STORE (data)); 195 | vk_search_music (query_text, GTK_LIST_STORE (data)); 196 | 197 | gtk_widget_set_sensitive (widget, TRUE); 198 | gtk_widget_grab_focus (widget); 199 | } 200 | 201 | static void 202 | on_my_music (GtkWidget *widget, gpointer *data) { 203 | gtk_widget_set_sensitive (widget, FALSE); 204 | 205 | last_search_query = NULL; 206 | gtk_list_store_clear (GTK_LIST_STORE (data)); 207 | vk_get_my_music (GTK_LIST_STORE (data)); 208 | 209 | gtk_widget_set_sensitive (widget, TRUE); 210 | } 211 | 212 | static void 213 | on_suggested_music (GtkWidget *widget, gpointer *data) { 214 | gtk_widget_set_sensitive (widget, FALSE); 215 | 216 | last_search_query = NULL; 217 | gtk_list_store_clear (GTK_LIST_STORE (data)); 218 | vk_get_recommended_music (GTK_LIST_STORE (data)); 219 | 220 | gtk_widget_set_sensitive (widget, TRUE); 221 | } 222 | 223 | static void 224 | on_filter_duplicates (GtkWidget *widget, gpointer *data) { 225 | vk_search_opts.filter_duplicates = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget)); 226 | } 227 | 228 | static void 229 | on_whole_phrase_search (GtkWidget *widget, gpointer *data) { 230 | vk_search_opts.search_whole_phrase = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget)); 231 | } 232 | 233 | static void 234 | on_search_target_changed (GtkWidget *widget, gpointer *data) { 235 | vk_search_opts.search_target = (VkSearchTarget) gtk_combo_box_get_active(GTK_COMBO_BOX (widget)); 236 | } 237 | 238 | static GtkCellRenderer * 239 | vk_gtk_cell_renderer_text_new_with_ellipsis () { 240 | GtkCellRenderer *renderer = gtk_cell_renderer_text_new (); 241 | g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); 242 | return renderer; 243 | } 244 | 245 | static 246 | GtkWidget * 247 | vk_create_browser_widget_content () { 248 | GtkWidget *dlg_vbox; 249 | GtkWidget *scroll_window; 250 | GtkWidget *search_hbox; 251 | GtkWidget *search_text; 252 | GtkWidget *search_target; 253 | GtkWidget *search_results; 254 | GtkListStore *list_store; 255 | GtkWidget *bottom_hbox; 256 | GtkWidget *my_music_button; 257 | GtkWidget *recommendations_button; 258 | GtkWidget *filter_duplicates; 259 | GtkWidget *search_whole_phrase; 260 | 261 | dlg_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); 262 | 263 | list_store = gtk_list_store_new (N_COLUMNS, 264 | G_TYPE_STRING, // ARTIST 265 | G_TYPE_STRING, // TITLE 266 | G_TYPE_INT, // DURATION seconds, not rendered 267 | G_TYPE_STRING, // DURATION_FORMATTED 268 | G_TYPE_STRING, // URL, not rendered 269 | G_TYPE_INT, // AID, not rendered 270 | G_TYPE_INT // OWNER_ID, not rendered 271 | ); 272 | 273 | search_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); 274 | gtk_box_pack_start (GTK_BOX (dlg_vbox), search_hbox, FALSE, FALSE, 0); 275 | 276 | search_text = gtk_entry_new (); 277 | gtk_widget_show (search_text); 278 | g_signal_connect(search_text, "activate", G_CALLBACK (on_search), list_store); 279 | gtk_box_pack_start (GTK_BOX (search_hbox), search_text, TRUE, TRUE, 0); 280 | 281 | search_target = gtk_combo_box_text_new (); 282 | // must to order of VkSearchTarget entries 283 | gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (search_target), "Anywhere"); 284 | gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (search_target), "Artist"); 285 | gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (search_target), "Title"); 286 | gtk_combo_box_set_active (GTK_COMBO_BOX (search_target), vk_search_opts.search_target); 287 | g_signal_connect (search_target, "changed", G_CALLBACK (on_search_target_changed), NULL); 288 | gtk_box_pack_start (GTK_BOX (search_hbox), search_target, FALSE, FALSE, 0); 289 | 290 | search_results = gtk_tree_view_new_with_model (GTK_TREE_MODEL (list_store)); 291 | gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (search_results), -1, "Artist", 292 | vk_gtk_cell_renderer_text_new_with_ellipsis (), 293 | "text", ARTIST_COLUMN, NULL); 294 | gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (search_results), -1, "Title", 295 | vk_gtk_cell_renderer_text_new_with_ellipsis (), 296 | "text", TITLE_COLUMN, NULL); 297 | gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (search_results), -1, "Duration", 298 | gtk_cell_renderer_text_new (), 299 | "text", DURATION_FORMATTED_COLUMN, NULL); 300 | 301 | //// Setup columns 302 | GtkTreeViewColumn *col; 303 | // artist col is resizeable and sortable 304 | col = gtk_tree_view_get_column (GTK_TREE_VIEW (search_results), 0); 305 | g_object_set (col, 306 | "sizing", GTK_TREE_VIEW_COLUMN_FIXED, 307 | "resizable", TRUE, 308 | "expand", TRUE, 309 | "sort-column-id", 0, 310 | NULL); 311 | // title col is resizeable, sortable and expanded 312 | col = gtk_tree_view_get_column (GTK_TREE_VIEW (search_results), 1); 313 | g_object_set (col, 314 | "sizing", GTK_TREE_VIEW_COLUMN_FIXED, 315 | "resizable", TRUE, 316 | "expand", TRUE, 317 | "sort-column-id", 1, 318 | NULL); 319 | // duration col is sortable and fixed width 320 | col = gtk_tree_view_get_column (GTK_TREE_VIEW (search_results), 2); 321 | g_object_set (col, 322 | "sizing", GTK_TREE_VIEW_COLUMN_FIXED, 323 | "resizable", TRUE, 324 | "min-width", 20, 325 | "max-width", 70, 326 | "fixed-width", 50, 327 | "sort-column-id", 2, 328 | NULL); 329 | 330 | 331 | gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (search_results)), 332 | GTK_SELECTION_MULTIPLE); 333 | 334 | g_signal_connect(search_results, "row-activated", G_CALLBACK (on_search_results_row_activate), NULL); 335 | g_signal_connect(search_results, "popup-menu", G_CALLBACK(on_search_results_popup_menu), NULL); 336 | g_signal_connect(search_results, "button-press-event", G_CALLBACK(on_search_results_button_press), NULL); 337 | 338 | scroll_window = gtk_scrolled_window_new (NULL, NULL); 339 | gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); 340 | gtk_container_add (GTK_CONTAINER (scroll_window), search_results); 341 | gtk_box_pack_start (GTK_BOX (dlg_vbox), scroll_window, TRUE, TRUE, 12); 342 | 343 | bottom_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); 344 | gtk_box_pack_start (GTK_BOX (dlg_vbox), bottom_hbox, FALSE, TRUE, 0); 345 | 346 | my_music_button = gtk_button_new_with_label ("My music"); 347 | g_signal_connect (my_music_button, "clicked", G_CALLBACK (on_my_music), list_store); 348 | gtk_box_pack_start (GTK_BOX (bottom_hbox), my_music_button, FALSE, FALSE, 0); 349 | 350 | recommendations_button = gtk_button_new_with_label ("Recommended"); 351 | g_signal_connect (recommendations_button, "clicked", G_CALLBACK (on_suggested_music), list_store); 352 | gtk_box_pack_start (GTK_BOX (bottom_hbox), recommendations_button, FALSE, FALSE, 0); 353 | 354 | filter_duplicates = gtk_check_button_new_with_label ("Filter duplicates"); 355 | gtk_widget_set_tooltip_text (filter_duplicates, "When checked removes duplicates during next search"); 356 | gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (filter_duplicates), vk_search_opts.filter_duplicates); 357 | g_signal_connect (filter_duplicates, "clicked", G_CALLBACK (on_filter_duplicates), list_store); 358 | gtk_box_pack_start (GTK_BOX (bottom_hbox), filter_duplicates, FALSE, FALSE, 0); 359 | 360 | search_whole_phrase = gtk_check_button_new_with_label ("Whole phrase"); 361 | gtk_widget_set_tooltip_text (search_whole_phrase, 362 | "Searching 'foo bar' would match exactly what you typed and not just 'foo' or 'bar'"); 363 | gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (search_whole_phrase), vk_search_opts.search_whole_phrase); 364 | g_signal_connect (search_whole_phrase, "clicked", G_CALLBACK (on_whole_phrase_search), list_store); 365 | gtk_box_pack_start (GTK_BOX (bottom_hbox), search_whole_phrase, FALSE, FALSE, 0); 366 | 367 | // refresh results when search criteria changed 368 | g_signal_connect (search_target, "changed", G_CALLBACK (maybe_do_search_again), search_text); 369 | g_signal_connect (filter_duplicates, "clicked", G_CALLBACK (maybe_do_search_again), search_text); 370 | g_signal_connect (search_whole_phrase, "clicked", G_CALLBACK (maybe_do_search_again), search_text); 371 | // save controls state to config 372 | g_signal_connect (search_target, "changed", 373 | G_CALLBACK (save_active_property_value_to_config), CONF_VK_UI_TARGET); 374 | g_signal_connect (filter_duplicates, "clicked", 375 | G_CALLBACK (save_active_property_value_to_config), CONF_VK_UI_DEDUP); 376 | g_signal_connect (search_whole_phrase, "clicked", 377 | G_CALLBACK (save_active_property_value_to_config), CONF_VK_UI_WHOLE_PHRASE); 378 | 379 | gtk_widget_show_all (dlg_vbox); 380 | return dlg_vbox; 381 | } 382 | 383 | GtkWidget * 384 | vk_create_browser_dialogue () { 385 | GtkWidget* add_tracks_dlg; 386 | GtkWidget* dlg_vbox; 387 | 388 | add_tracks_dlg = gtk_dialog_new_with_buttons ( 389 | "Search tracks", 390 | GTK_WINDOW (gtkui_plugin->get_mainwin ()), 391 | 0, 392 | NULL, 393 | NULL); 394 | gtk_container_set_border_width (GTK_CONTAINER (add_tracks_dlg), 12); 395 | gtk_window_set_default_size (GTK_WINDOW (add_tracks_dlg), 840, 400); 396 | dlg_vbox = gtk_dialog_get_content_area (GTK_DIALOG (add_tracks_dlg)); 397 | gtk_box_pack_start (GTK_BOX (dlg_vbox), vk_create_browser_widget_content (), TRUE, TRUE, 0); 398 | return add_tracks_dlg; 399 | } 400 | 401 | void 402 | vk_setup_browser_widget (ddb_gtkui_widget_t *w) { 403 | // wrap into EventBox for proper design mode widget detection 404 | w->widget = gtk_event_box_new (); 405 | gtk_widget_set_can_focus (w->widget, FALSE); 406 | gtk_container_add (GTK_CONTAINER (w->widget), vk_create_browser_widget_content ()); 407 | 408 | gtkui_plugin->w_override_signals (w->widget, w); 409 | } 410 | 411 | gboolean 412 | show_message (GtkMessageType messageType, const gchar *message) { 413 | GtkWidget *dlg; 414 | 415 | dlg = gtk_message_dialog_new (NULL, 416 | GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, 417 | messageType, 418 | GTK_BUTTONS_OK, 419 | "%s", 420 | message); 421 | g_signal_connect_swapped (dlg, "response", G_CALLBACK (gtk_widget_destroy), dlg); 422 | gtk_dialog_run (GTK_DIALOG (dlg) ); 423 | return FALSE; 424 | } 425 | -------------------------------------------------------------------------------- /src/ui.h: -------------------------------------------------------------------------------- 1 | /* 2 | * ui.h 3 | * 4 | * Created on: Dec 9, 2012 5 | * Author: scorp 6 | */ 7 | 8 | #ifndef UI_H_ 9 | #define UI_H_ 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | G_BEGIN_DECLS 16 | 17 | /** 18 | * List view columns. 19 | */ 20 | enum { 21 | ARTIST_COLUMN = 0, 22 | TITLE_COLUMN, 23 | DURATION_COLUMN, 24 | DURATION_FORMATTED_COLUMN, 25 | URL_COLUMN, 26 | AID_COLUMN, 27 | OWNER_ID_COLUMN, 28 | N_COLUMNS 29 | }; 30 | 31 | typedef enum { 32 | VK_TARGET_ANY_FIELD = 0, 33 | VK_TARGET_ARTIST_FIELD, 34 | VK_TARGET_TITLE_FIELD 35 | } VkSearchTarget; 36 | 37 | struct { 38 | gboolean filter_duplicates; 39 | gboolean search_whole_phrase; 40 | VkSearchTarget search_target; 41 | } vk_search_opts; 42 | 43 | #define CONF_VK_UI_DEDUP "vk.ui.filter.duplicates" 44 | #define CONF_VK_UI_WHOLE_PHRASE "vk.ui.whole.phrase" 45 | #define CONF_VK_UI_TARGET "vk.uk.target" 46 | 47 | 48 | gboolean show_message (GtkMessageType messageType, const gchar *message); 49 | GtkWidget * vk_create_browser_dialogue (); 50 | void vk_setup_browser_widget (ddb_gtkui_widget_t *w); 51 | 52 | G_END_DECLS 53 | #endif /* UI_H_ */ 54 | -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | /* 2 | * util.c 3 | * 4 | * Created on: Dec 9, 2012 5 | * Author: scorp 6 | */ 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #if (__STDC_VERSION__ >= 199901L) 13 | #include 14 | #endif 15 | 16 | #include "common-defs.h" 17 | 18 | 19 | static size_t 20 | http_write_data (char *ptr, size_t size, size_t nmemb, void *userdata) { 21 | g_string_append_len ((GString *) userdata, ptr, size * nmemb); 22 | return size * nmemb; 23 | } 24 | 25 | gchar * 26 | http_get_string (const gchar *url, GError **error) { 27 | CURL *curl; 28 | GString *resp_str; 29 | char curl_err_buf[CURL_ERROR_SIZE]; 30 | 31 | curl = curl_easy_init (); 32 | resp_str = g_string_sized_new (1024 * 3); 33 | 34 | 35 | trace ("Requesting URL %s\n", url); 36 | 37 | curl_easy_setopt (curl, CURLOPT_URL, url); 38 | curl_easy_setopt (curl, CURLOPT_USERAGENT, "DeadBeef"); 39 | curl_easy_setopt (curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); 40 | // enable up to 10 redirects 41 | curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, 1); 42 | curl_easy_setopt (curl, CURLOPT_MAXREDIRS, 10); 43 | // setup handlers 44 | curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, http_write_data); 45 | curl_easy_setopt (curl, CURLOPT_WRITEDATA, resp_str); 46 | curl_easy_setopt (curl, CURLOPT_ERRORBUFFER, curl_err_buf); 47 | 48 | int status = curl_easy_perform (curl); 49 | if (status != 0) { 50 | trace ("Curl error: %s\n", curl_err_buf); 51 | *error = g_error_new (g_quark_from_static_string ("vk plugin curl error"), 52 | status, 53 | "%s", 54 | curl_err_buf); 55 | } 56 | 57 | curl_easy_cleanup (curl); 58 | 59 | // return NULL in case of curl error, char buffer otherwise 60 | return g_string_free (resp_str, status != 0); 61 | } 62 | 63 | char * 64 | repl_str(const char *str, const char *from, const char *to) { 65 | 66 | /* Adjust each of the below values to suit your needs. */ 67 | 68 | /* Increment positions cache size initially by this number. */ 69 | size_t cache_sz_inc = 16; 70 | /* Thereafter, each time capacity needs to be increased, 71 | * multiply the increment by this factor. */ 72 | const size_t cache_sz_inc_factor = 3; 73 | /* But never increment capacity by more than this number. */ 74 | const size_t cache_sz_inc_max = 1048576; 75 | 76 | char *pret, *ret = NULL; 77 | const char *pstr2, *pstr = str; 78 | size_t i, count = 0; 79 | #if (__STDC_VERSION__ >= 199901L) 80 | uintptr_t *pos_cache_tmp, *pos_cache = NULL; 81 | #else 82 | ptrdiff_t *pos_cache_tmp, *pos_cache = NULL; 83 | #endif 84 | size_t cache_sz = 0; 85 | size_t cpylen, orglen, retlen, tolen, fromlen = strlen(from); 86 | 87 | /* Find all matches and cache their positions. */ 88 | while ((pstr2 = strstr(pstr, from)) != NULL) { 89 | count++; 90 | 91 | /* Increase the cache size when necessary. */ 92 | if (cache_sz < count) { 93 | cache_sz += cache_sz_inc; 94 | pos_cache_tmp = realloc(pos_cache, sizeof(*pos_cache) * cache_sz); 95 | if (pos_cache_tmp == NULL) { 96 | goto end_repl_str; 97 | } else pos_cache = pos_cache_tmp; 98 | cache_sz_inc *= cache_sz_inc_factor; 99 | if (cache_sz_inc > cache_sz_inc_max) { 100 | cache_sz_inc = cache_sz_inc_max; 101 | } 102 | } 103 | 104 | pos_cache[count-1] = pstr2 - str; 105 | pstr = pstr2 + fromlen; 106 | } 107 | 108 | orglen = pstr - str + strlen(pstr); 109 | 110 | /* Allocate memory for the post-replacement string. */ 111 | if (count > 0) { 112 | tolen = strlen(to); 113 | retlen = orglen + (tolen - fromlen) * count; 114 | } else retlen = orglen; 115 | ret = malloc(retlen + 1); 116 | if (ret == NULL) { 117 | goto end_repl_str; 118 | } 119 | 120 | if (count == 0) { 121 | /* If no matches, then just duplicate the string. */ 122 | strcpy(ret, str); 123 | } else { 124 | /* Otherwise, duplicate the string whilst performing 125 | * the replacements using the position cache. */ 126 | pret = ret; 127 | memcpy(pret, str, pos_cache[0]); 128 | pret += pos_cache[0]; 129 | for (i = 0; i < count; i++) { 130 | memcpy(pret, to, tolen); 131 | pret += tolen; 132 | pstr = str + pos_cache[i] + fromlen; 133 | cpylen = (i == count-1 ? orglen : pos_cache[i+1]) - pos_cache[i] - fromlen; 134 | memcpy(pret, pstr, cpylen); 135 | pret += cpylen; 136 | } 137 | ret[retlen] = '\0'; 138 | } 139 | 140 | end_repl_str: 141 | /* Free the cache and return the post-replacement string, 142 | * which will be NULL in the event of an error. */ 143 | free(pos_cache); 144 | return ret; 145 | } 146 | 147 | 148 | const gchar * 149 | strip_prefix(const gchar *haystack, const gchar *needle) { 150 | if (g_str_has_prefix (haystack, needle)) { 151 | return haystack + strlen (needle); 152 | } 153 | return NULL; 154 | } 155 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by scorp on 10/2/16. 3 | // 4 | 5 | #ifndef DB_VK_UTIL_H 6 | #define DB_VK_UTIL_H 7 | G_BEGIN_DECLS 8 | 9 | #include 10 | 11 | 12 | gchar *http_get_string (const gchar *url, GError **error); 13 | 14 | /** 15 | * Replaces in the string str all the occurrences of the source string from with the destination string to. 16 | * The lengths of the strings from and to may differ. The string to may be of any length, but the string 17 | * from must be of non-zero length - the penalty for providing an empty string for the from parameter is an 18 | * infinite loop. In addition, none of the three parameters may be NULL. 19 | * 20 | * http://creativeandcritical.net/str-replace-c 21 | * @return The post-replacement string, or NULL if memory for the new string could not be allocated. Does 22 | * not modify the original string. The memory for the returned post-replacement string may be 23 | * deallocated with the standard library function free when it is no longer required. 24 | */ 25 | char *repl_str(const char *str, const char *from, const char *to); 26 | 27 | /** 28 | * @return pointer to a position in haystack right after prefix or NULL. 29 | */ 30 | const gchar *strip_prefix (const gchar *haystack, const gchar *prefix); 31 | 32 | G_END_DECLS 33 | #endif //DB_VK_UTIL_H 34 | -------------------------------------------------------------------------------- /src/vk-api.c: -------------------------------------------------------------------------------- 1 | /* 2 | * vk-api.c 3 | * 4 | * Created on: Dec 9, 2012 5 | * Author: scorp 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "common-defs.h" 13 | #include "vk-api.h" 14 | 15 | 16 | #define VK_API_DOMAIN_STR "vkontakte api for deadbeef" 17 | /* 18 | * {"error":{ 19 | * "error_code":5, 20 | * "error_msg":"User authorization failed: invalid access_token.", 21 | * "request_params":[ 22 | * {"key":"oauth","value":"1"}, 23 | * {"key":"method","value":"audio.get"}, 24 | * {"key":"access_token","value""... 25 | */ 26 | #define VK_ERR_JSON_KEY "error" 27 | #define VK_ERR_JSON_CODE_KEY "error_code" 28 | #define VK_ERR_JSON_MSG_KEY "error_msg" 29 | #define VK_ERR_JSON_EXTRA_KEY "request_params" 30 | 31 | const gchar *VK_PUBLIC_SITE_PREFIXES[] = { 32 | "https://vk.com/", 33 | "http://vk.com/", 34 | NULL 35 | }; 36 | 37 | /** 38 | * Checks if response contains error, returns TRUE and sets error appropriately in 39 | * case of failure. Just returns FALSE otherwise. 40 | * 41 | * @return TRUE if response contains failure, FALSE otherwise. 42 | */ 43 | static gboolean 44 | vk_error_check (json_t *root, GError **error) { 45 | 46 | if (!json_is_object (root) ) { 47 | *error = g_error_new_literal (g_quark_from_static_string (VK_API_DOMAIN_STR), 48 | -1, 49 | "Root should be a JSON object"); 50 | return FALSE; 51 | } 52 | 53 | json_t *error_response = json_object_get (root, VK_ERR_JSON_KEY); 54 | if (error_response) { 55 | json_t *error_message = json_object_get (error_response, VK_ERR_JSON_MSG_KEY); 56 | assert (error_message); 57 | 58 | *error = g_error_new_literal (g_quark_from_static_string (VK_API_DOMAIN_STR), 59 | (gint) json_integer_value (json_object_get (root, VK_ERR_JSON_CODE_KEY)), 60 | g_strdup (json_string_value (error_message)) ); 61 | return FALSE; 62 | } 63 | return TRUE; 64 | } 65 | 66 | static void 67 | json_error_to_g_error(json_error_t *json_error, GError **error) { 68 | *error = g_error_new ( 69 | g_quark_from_static_string (VK_API_DOMAIN_STR), 70 | 0, 71 | "%s. Line: %d, column %d", 72 | json_error->text, 73 | json_error->line, 74 | json_error->column 75 | ); 76 | } 77 | 78 | static gboolean 79 | vk_audio_track_parse (json_t *tracks_array, size_t index_, VkAudioTrack *audio_track) { 80 | json_t *json_track; 81 | 82 | json_track = json_array_get (tracks_array, index_); 83 | 84 | // first element may contain number of elements 85 | if (0 == index_ && !json_is_object (json_track)) { 86 | return FALSE; 87 | } 88 | 89 | assert (json_is_object (json_track)); 90 | 91 | // read data from json 92 | audio_track->aid = (int) json_integer_value (json_object_get (json_track, "aid")); 93 | audio_track->artist = json_string_value (json_object_get (json_track, "artist")); 94 | audio_track->duration = (int) json_integer_value (json_object_get (json_track, "duration")); 95 | audio_track->owner_id = (int) json_integer_value (json_object_get (json_track, "owner_id")); 96 | audio_track->title = json_string_value (json_object_get (json_track, "title")); 97 | audio_track->url = json_string_value (json_object_get (json_track, "url")); 98 | 99 | return TRUE; 100 | } 101 | 102 | gboolean 103 | vk_audio_response_parse (const gchar *json, 104 | VkAudioTrackCallback callback, 105 | gpointer userdata, 106 | GError **error) { 107 | json_t *root; 108 | json_error_t json_error; 109 | json_t *tmp_node; 110 | 111 | root = json_loads(json, 0, &json_error); 112 | 113 | if (!root) { 114 | trace("Unable to parse audio response: %s\n", json_error.text); 115 | json_error_to_g_error (&json_error, error); 116 | return FALSE; 117 | } 118 | 119 | if (!vk_error_check (root, error)) { 120 | trace("Error from VK: %d, %s\n", (*error)->code, (*error)->message); 121 | json_decref (root); 122 | return FALSE; 123 | } 124 | 125 | assert(json_is_object (root)); 126 | tmp_node = json_object_get (root, "response"); 127 | assert(json_is_array (tmp_node)); 128 | 129 | for (size_t i = 0; i < json_array_size (tmp_node); i++) { 130 | VkAudioTrack audio_track; 131 | if (vk_audio_track_parse (tmp_node, i, &audio_track)) { 132 | callback (&audio_track, i, userdata); 133 | } 134 | } 135 | 136 | json_decref (root); 137 | return TRUE; 138 | } 139 | 140 | VkAuthData * 141 | vk_auth_data_parse (const gchar *auth_data_str) { 142 | if (auth_data_str == NULL || strlen (auth_data_str) == 0) { 143 | trace ("VK auth data missing\n"); 144 | return NULL ; 145 | } 146 | VkAuthData *vk_auth_data = NULL; 147 | json_t *root; 148 | json_error_t error; 149 | 150 | root = json_loads(auth_data_str, 0, &error); 151 | if (!root) { 152 | trace ("VK auth data invalid\n"); 153 | return NULL; 154 | } 155 | 156 | if (json_is_object (root)) { 157 | vk_auth_data = g_malloc (sizeof *vk_auth_data); 158 | vk_auth_data->access_token = g_strdup (json_string_value (json_object_get (root, "access_token"))); 159 | vk_auth_data->user_id = json_integer_value (json_object_get (root, "user_id")); 160 | vk_auth_data->expires_in = json_integer_value (json_object_get (root, "expires_in")); 161 | } 162 | 163 | json_decref(root); 164 | return vk_auth_data; 165 | } 166 | 167 | void 168 | vk_auth_data_free (VkAuthData *vk_auth_data) { 169 | if (vk_auth_data != NULL) { 170 | g_free ((gchar *) vk_auth_data->access_token); 171 | g_free (vk_auth_data); 172 | } 173 | } 174 | 175 | glong 176 | vk_utils_resolve_screen_name_parse (const gchar *json, GError **error) { 177 | json_error_t json_error; 178 | json_t *root; 179 | json_t *response; 180 | glong id = 0; 181 | 182 | root = json_loads (json, 0, &json_error); 183 | if (!root) { 184 | trace("Unable to parse audio response: %s\n", json_error.text); 185 | json_error_to_g_error (&json_error, error); 186 | return FALSE; 187 | } 188 | 189 | if (!vk_error_check (root, error)) { 190 | trace("Error from VK: %d, %s\n", (*error)->code, (*error)->message); 191 | json_decref (root); 192 | return FALSE; 193 | } 194 | 195 | assert(json_is_object (root)); 196 | response = json_object_get (root, "response"); 197 | if (!json_is_object (response)) { 198 | trace("Screen name was not found\n"); 199 | } else { 200 | const char *object_type = json_string_value (json_object_get (response, "type")); 201 | const long object_id = json_integer_value (json_object_get (response, "object_id")); 202 | 203 | if (g_strcmp0 (object_type, "user") == 0) { 204 | id = object_id; 205 | } else if (g_strcmp0 (object_type, "group") == 0) { 206 | id = -object_id; 207 | } else { 208 | trace("Unexpected object type: %s\n", json); 209 | } 210 | } 211 | 212 | json_decref (root); 213 | return id; 214 | } -------------------------------------------------------------------------------- /src/vk-api.h: -------------------------------------------------------------------------------- 1 | /* 2 | * vk-api.h 3 | * 4 | * Created on: Dec 9, 2012 5 | * Author: scorp 6 | */ 7 | 8 | #ifndef VK_API_H_ 9 | #define VK_API_H_ 10 | G_BEGIN_DECLS 11 | 12 | 13 | #define VK_API_URL "https://api.vk.com/method" 14 | /** Search arbitrary tracks */ 15 | #define VK_API_METHOD_AUDIO_SEARCH VK_API_URL "/audio.search" 16 | /** Retrieve 'My music' contents */ 17 | #define VK_API_METHOD_AUDIO_GET VK_API_URL "/audio.get" 18 | /** Retrieve details of a track by it's ID */ 19 | #define VK_API_METHOD_AUDIO_GET_BY_ID VK_API_URL "/audio.getById" 20 | /** Retrieve 'Suggested music' contents */ 21 | #define VK_API_METHOD_AUDIO_GET_RECOMMENDATIONS VK_API_URL "/audio.getRecommendations" 22 | /** Resolve vk.com object by screen name (alias) */ 23 | #define VK_API_METHOD_UTILS_RESOLVE_SCREEN_NAME VK_API_URL "/utils.resolveScreenName" 24 | 25 | /** Prefixes of vk.com public site links. */ 26 | extern const gchar *VK_PUBLIC_SITE_PREFIXES[]; 27 | 28 | typedef struct { 29 | const gchar *access_token; 30 | glong user_id; 31 | glong expires_in; 32 | } VkAuthData; 33 | 34 | 35 | #define VK_AUDIO_MAX_TRACKS 300 36 | typedef struct { 37 | int aid; 38 | int owner_id; 39 | const gchar *artist; 40 | const gchar *title; 41 | int duration; // seconds 42 | const gchar *url; 43 | } VkAudioTrack; 44 | 45 | typedef void (*VkAudioTrackCallback) (VkAudioTrack *track, size_t index, gpointer userdata); 46 | 47 | /** 48 | * Tries to parse given authentication data. Expects following JSON structure: 49 | * { 50 | * 'access_token': '...', 51 | * 'expires_in': 999999, 52 | * 'user_id': 9999999 53 | * } 54 | * 55 | * Returned instance should be disposed with vk_auth_data_free. 56 | * 57 | * @param auth_data_str authentication data string. 58 | * @return VkAuthData or NULL. 59 | */ 60 | VkAuthData * vk_auth_data_parse (const gchar *auth_data_str); 61 | /** 62 | * Dispose VkAuthData structures. Accepts NULLs. 63 | */ 64 | void vk_auth_data_free (VkAuthData *vk_auth_data); 65 | 66 | /** 67 | * Parse response of VK_API_METHOD_AUDIO_SEARCH or VK_API_METHOD_AUDIO_GET methods. 68 | * If FALSE was returned called is responsible for calling g_error_free on error 69 | * parameter. 70 | * 71 | * @return FALSE if error occurred, error details stored in error parameter. 72 | * TRUE otherwise 73 | */ 74 | gboolean vk_audio_response_parse (const gchar *json, 75 | VkAudioTrackCallback callback, 76 | gpointer userdata, 77 | GError **error); 78 | 79 | /** 80 | * Parse response of VK_API_METHOD_UTILS_RESOLVE_SCREEN_NAME method. 81 | * 82 | * @return a positive or negative or zero value if object type is user, group or other. 83 | */ 84 | glong vk_utils_resolve_screen_name_parse (const gchar *json, 85 | GError **error); 86 | 87 | G_END_DECLS 88 | #endif /* VK_API_H_ */ 89 | -------------------------------------------------------------------------------- /src/vkontakte_plugin.c: -------------------------------------------------------------------------------- 1 | // disable gdk_thread_enter\leave warnings 2 | #define GDK_VERSION_MIN_REQUIRED GDK_VERSION_3_4 3 | 4 | #define DDB_WARN_DEPRECATED 1 5 | #define DDB_API_LEVEL 6 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "common-defs.h" 12 | #include "core.h" 13 | 14 | 15 | static DB_functions_t *deadbeef; 16 | static ddb_gtkui_t *gtkui_plugin; 17 | static DB_vfs_t *vfs_curl_plugin; 18 | 19 | 20 | static const char *scheme_names[] = { "vk://", NULL }; 21 | static const char ** 22 | vk_ddb_vfs_get_schemes () { 23 | return scheme_names; 24 | } 25 | 26 | static DB_FILE * 27 | vk_ddb_vfs_open (const char *fname) { 28 | return vk_vfs_open (fname); 29 | } 30 | 31 | static int 32 | vk_ddb_vfs_is_streaming () { 33 | return 1; 34 | } 35 | 36 | static int 37 | vk_ddb_action_callback(DB_plugin_action_t *action, int ctx) { 38 | g_idle_add (vk_action_gtk, NULL); 39 | return 0; 40 | } 41 | 42 | static int 43 | vk_ddb_connect () { 44 | vfs_curl_plugin = (DB_vfs_t *) deadbeef->plug_get_for_id ("vfs_curl"); 45 | if (!vfs_curl_plugin) { 46 | trace ("cURL VFS plugin required\n"); 47 | return -1; 48 | } 49 | 50 | gtkui_plugin = (ddb_gtkui_t *) deadbeef->plug_get_for_id (DDB_GTKUI_PLUGIN_ID); 51 | 52 | if (gtkui_plugin && gtkui_plugin->gui.plugin.version_major == 2) { // gtkui version 2 53 | vk_initialise (deadbeef, gtkui_plugin); 54 | gtkui_plugin->w_reg_widget ("VK Browser", DDB_WF_SINGLE_INSTANCE, w_vkbrowser_create, "vkbrowser", NULL); 55 | vk_config_changed (); // refresh config at start 56 | return 0; 57 | } 58 | 59 | return -1; 60 | } 61 | 62 | static DB_plugin_action_t vk_ddb_action = { 63 | .title = "File/Add tracks from VK", 64 | .name = "vk_add_tracks", 65 | .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU, 66 | .callback2 = (DB_plugin_action_callback2_t) vk_ddb_action_callback, 67 | .next = NULL, 68 | }; 69 | 70 | static DB_plugin_action_t * 71 | vk_ddb_get_actions(DB_playItem_t *it) { 72 | return &vk_ddb_action; 73 | } 74 | 75 | /** 76 | * DeadBeef messages handler. 77 | * 78 | * @param id message id. 79 | * @param ctx ? 80 | * @param p1 ? 81 | * @param p2 ? 82 | * @return ? 83 | */ 84 | static int 85 | vk_ddb_message (uint32_t id, uintptr_t ctx, uint32_t p1, uint32_t p2) { 86 | switch (id) { 87 | case DB_EV_CONFIGCHANGED: 88 | vk_config_changed(); 89 | break; 90 | default: 91 | break; 92 | } 93 | return 0; 94 | } 95 | 96 | static int 97 | vk_ddb_disconnect () { 98 | vk_perform_cleanup (); 99 | 100 | if (gtkui_plugin) { 101 | gtkui_plugin->w_unreg_widget ("vkbrowser"); 102 | } 103 | 104 | gtkui_plugin = NULL; 105 | vfs_curl_plugin = NULL; 106 | return 0; 107 | } 108 | 109 | static const char vk_ddb_config_dialog[] = 110 | "property \"Navigate to the URL in text box\n(don't change the URL here)\" entry " CONF_VK_AUTH_URL " " VK_AUTH_URL ";\n" 111 | "property \"Paste data from the page here\" entry " CONF_VK_AUTH_DATA " \"\";\n" 112 | "property \"Automatically fallback to plain HTTP for tracks\n(VK API still over HTTPS)\" checkbox " CONF_TRACKS_FORCE_HTTP " 0;\n" 113 | ; 114 | 115 | DB_vfs_t plugin = { 116 | DDB_REQUIRE_API_VERSION(1, 5) 117 | .plugin.type = DB_PLUGIN_VFS, 118 | .plugin.version_major = 0, 119 | .plugin.version_minor = 2, 120 | #if GTK_CHECK_VERSION(3,0,0) 121 | .plugin.id = "vkontakte_3", 122 | #else 123 | .plugin.id = "vkontakte_2", 124 | #endif 125 | .plugin.name = "VKontakte", 126 | .plugin.descr = "Play music from VKontakte social network site.\n", 127 | .plugin.copyright = "Kirill Malyshev", 128 | .plugin.website = "http://scorpp.github.io/db-vk/", 129 | // callbacks 130 | .plugin.configdialog = vk_ddb_config_dialog, 131 | .plugin.connect = vk_ddb_connect, 132 | .plugin.disconnect = vk_ddb_disconnect, 133 | .plugin.message = vk_ddb_message, 134 | .plugin.get_actions = vk_ddb_get_actions, 135 | // overriding minimum methods of a VFS plugin since vfs_curl will do all 136 | // the work for us. files opened with vkontakte plugin will actually look 137 | // as those opened by vfs_curl. 138 | .get_schemes = vk_ddb_vfs_get_schemes, 139 | .is_streaming = vk_ddb_vfs_is_streaming, 140 | .open = vk_ddb_vfs_open 141 | }; 142 | 143 | DB_plugin_t * 144 | #if GTK_CHECK_VERSION(3,0,0) 145 | vkontakte_gtk3_load (DB_functions_t *api) { 146 | #else 147 | vkontakte_gtk2_load (DB_functions_t *api) { 148 | #endif 149 | deadbeef = api; 150 | return DB_PLUGIN (&plugin); 151 | } 152 | --------------------------------------------------------------------------------