1 (изменено: 1srafel, 2025-10-23 06:36:06)

Тема: AHK v1: Обновить состояние скрипта, скачивающего посты ВК

Составил с ИИ код ниже, который за 1 запрос может скачивать до 100 постов из VK с форматированием в HTML-формат различных типов ВК-постов. Для запроса используется приведенная "хранимая процедура", которую нужно создать на сайте - и токен, прописываемый в файл "token.txt". Так же используется библиотека JSON.ahk.

Код имеет странную проблему, когда после парсинга json-ответа первой итерации (offset := 0), на второй итерации (offset := 1) json-ответ не распознается как валидный. При этом достоверно известно, что все последующие ответы валидны, что проверялось подменой ответа первой итерации - ответом второй итерации, а так же при старте скрипта - запуском парсинга со второй итерации. Пытался очищать предыдущее состояние всех данных, но помогает только перезагрузка скрипта, когда JSON класс полностью обновляется. Просьба подсказать решение задачи с полным обновлением состояния скрипта после каждой итерации.

Хранимая процедура

// execute.getMaxPosts
var response = API.wall.get({
    "owner_id": Args.owner_id,
    "offset": Args.offset,
    "count": 25,  // 100 — МАКСИМУМ ДЛЯ WALL.GET
    "extended": 1,
    "fields": "first_name,last_name",
    "v": "5.291"
});

return response;

AHK v1
(Первая часть кода)


#NoEnv
SendMode Input
#SingleInstance Force
SetWorkingDir %A_ScriptDir%

; Подключаем JSON.ahk
#Include JSON.ahk

; Чтение токена из файла
FileRead, access_token, token.txt
if (ErrorLevel || access_token = "") {
    MsgBox, Не удалось прочитать токен из файла token.txt
    ExitApp
}

; Убираем возможные пробелы и переводы строк
access_token := Trim(access_token)

owner_id := "-29534144"
output_folder := "VK_Posts_HTML"

; Создаем папку для сохранения
FileCreateDir, %output_folder%

; Получаем и сохраняем все посты
total_posts := GetAllPostsAndSaveHTML(access_token, owner_id, output_folder)
MsgBox, % "Готово! Сохранено постов: " total_posts

; Основная функция для получения всех постов
GetAllPostsAndSaveHTML(access_token, owner_id, output_folder) {
    total_saved := 0
    batch_size := 25
    offset := 0
    total_count := 0  ; Добавляем переменную для общего количества постов
    
    Loop {
        ; Получаем пачку постов через execute
        posts_data := GetPostsBatch(access_token, owner_id, offset)
        if (!posts_data || !posts_data.items || posts_data.items.MaxIndex() = 0) {
            break
        }
        
        ; Сохраняем общее количество постов при первом запросе
        if (offset = 0 && posts_data.count > 0) {
            total_count := posts_data.count
        }
        
        ; Обрабатываем каждый пост
        for index, post_item in posts_data.items {
            ; Парсим пост в наш формат
            post_data := ParsePostItem(post_item, posts_data)
            if (post_data && post_data.id) {
                html_content := ConvertPostToHTML(post_data)
                filename := output_folder "\" owner_id "_" post_data.id ".html"
                SaveHTMLToFile(html_content, filename)
                total_saved++
                
                ; Прогресс с информацией об общем количестве
                progress_text := "Сохранено: " total_saved " из " total_count " постов (offset: " offset ")"
                if (total_count > 0) {
                    percent := Round((total_saved / total_count) * 100)
                    progress_text .= " (" percent "%)"
                }
                ToolTip, % progress_text
            }
        }
        
        offset += batch_size ; Увеличиваем offset для следующей пачки
        Sleep, 2500  ; Пауза между запросами
        
        ; если сохранили больше или равно общему количеству постов - выходим
        if (total_count > 0 && total_saved >= total_count) {
            break
        }
        
        ; Дополнительная проверка: если в ответе нет постов
        if (posts_data.items.MaxIndex() = 0) {
            break
        }
    }
    
    ToolTip
    return total_saved
}

; Получение пачки постов через execute.getMaxPosts
GetPostsBatch(access_token, owner_id, offset) {
    url := "https://api.vk.com/method/execute.getMaxPosts?owner_id=" owner_id
        . "&offset=" offset
        . "&access_token=" access_token
        . "&v=5.291"
    
    response := SendHTTPRequest(url)
    
    ; Сохраняем сырой ответ для отладки
    FileDelete, debug_batch_%offset%_response.txt
    FileAppend, %response%, debug_batch_%offset%_response.txt, UTF-8
    
    if (response = "ERROR") {
        return ""
    }
    
    return ParsePostsResponse(response)
}

; Парсинг ответа от execute.getMaxPosts
ParsePostsResponse(response) {
    try {
        json := JSON.Load(response)
        
        ; Проверяем на ошибки API
        if (json.error) {
            error_msg := "Ошибка API: код " json.error.error_code " - " json.error.error_msg
            MsgBox, % error_msg
            return ""
        }
        
        ; Проверяем наличие response
        if (!json.response) {
            MsgBox, Нет response в ответе
            return ""
        }
        
        result := Object()
        result.items := json.response.items ? json.response.items : []
        result.profiles := json.response.profiles ? json.response.profiles : []
        result.groups := json.response.groups ? json.response.groups : []
        result.count := json.response.count ? json.response.count : 0  ; Это поле теперь используется
        
        return result
        
    } catch e {
        MsgBox, Ошибка парсинга JSON батча: %e%
        return ""
    }
}

; Конвертация элемента поста в наш формат
ParsePostItem(post_item, posts_data) {
    post := Object()
    post.photos := []
    post.links := []
    post.videos := []
    post.audios := []
    post.documents := []
    post.polls := []
    post.reposts := []
    post.albums := []
    
    ; Основные поля поста
    post.id := post_item.id
    post.owner_id := post_item.owner_id
    post.from_id := post_item.from_id
    post.date := post_item.date
    post.text := post_item.text ? post_item.text : ""
    post.title := post_item.title ? post_item.title : ""
    
    ; Статистика
    post.likes := post_item.likes.count
    post.reposts := post_item.reposts.count
    post.comments := post_item.comments.count
    post.views := post_item.views.count
    
    ; Извлекаем все виды вложений
    if (post_item.attachments && post_item.attachments.Length() > 0) {
        for index, attachment in post_item.attachments {
            if (attachment.type = "photo" && attachment.photo) {
                photo_url := GetLargestPhotoUrl(attachment.photo)
                if (photo_url) {
                    post.photos.Push(photo_url)
                }
            }
            else if (attachment.type = "link" && attachment.link) {
                link_data := ExtractLinkData(attachment.link)
                if (link_data) {
                    post.links.Push(link_data)
                }
            }
            else if (attachment.type = "video" && attachment.video) {
                video_data := ExtractVideoData(attachment.video)
                if (video_data) {
                    post.videos.Push(video_data)
                }
            }
            else if (attachment.type = "audio" && attachment.audio) {
                audio_data := ExtractAudioData(attachment.audio)
                if (audio_data) {
                    post.audios.Push(audio_data)
                }
            }
            else if (attachment.type = "doc" && attachment.doc) {
                doc_data := ExtractDocumentData(attachment.doc)
                if (doc_data) {
                    post.documents.Push(doc_data)
                }
            }
            else if (attachment.type = "poll" && attachment.poll) {
                poll_data := ExtractPollData(attachment.poll)
                if (poll_data) {
                    post.polls.Push(poll_data)
                }
            }
            else if (attachment.type = "post" && attachment.wall) {
                repost_data := ExtractRepostData(attachment.wall)
                if (repost_data) {
                    post.reposts.Push(repost_data)
                }
            }
            else if (attachment.type = "album" && attachment.album) {
                album_data := ExtractAlbumData(attachment.album)
                if (album_data) {
                    post.albums.Push(album_data)
                }
            }
        }
    }
    
    ; Название группы и screen_name
    post.group_name := "Группа"
    post.group_screen_name := ""
    post.group_avatar := ""
    if (posts_data.groups && posts_data.groups.Length() > 0) {
        post.group_name := posts_data.groups[1].name
        post.group_screen_name := posts_data.groups[1].screen_name ? posts_data.groups[1].screen_name : ""
        ; Извлекаем аватар группы (лучшее качество)
        if (posts_data.groups[1].photo_200) {
            post.group_avatar := posts_data.groups[1].photo_200
        } else if (posts_data.groups[1].photo_100) {
            post.group_avatar := posts_data.groups[1].photo_100
        } else if (posts_data.groups[1].photo_50) {
            post.group_avatar := posts_data.groups[1].photo_50
        }
    }
    
    ; Если это пост от пользователя, ищем его аватар
    post.user_avatar := ""
    if (post.from_id > 0 && posts_data.profiles && posts_data.profiles.Length() > 0) {
        for index, profile in posts_data.profiles {
            if (profile.id = post.from_id) {
                ; Аватар пользователя (лучшее качество)
                if (profile.photo_200) {
                    post.user_avatar := profile.photo_200
                } else if (profile.photo_100) {
                    post.user_avatar := profile.photo_100
                } else if (profile.photo_50) {
                    post.user_avatar := profile.photo_50
                }
                break
            }
        }
    }
    
    return post
}

; Извлечение данных ссылки
ExtractLinkData(link) {
    link_data := Object()
    link_data.url := link.url
    link_data.title := link.title ? link.title : ""
    link_data.description := link.description ? link.description : ""
    link_data.caption := link.caption ? link.caption : ""
    
    ; Извлекаем превью ссылки
    if (link.photo) {
        link_data.preview_url := GetLargestPhotoUrl(link.photo)
    } else {
        link_data.preview_url := ""
    }
    
    return link_data
}

; Извлечение данных видео
ExtractVideoData(video) {
    video_data := Object()
    video_data.id := video.id
    video_data.owner_id := video.owner_id
    video_data.title := video.title ? video.title : ""
    video_data.description := video.description ? video.description : ""
    video_data.duration := video.duration ? video.duration : 0
    
    ; Превью видео
    if (video.image && video.image.Length() > 0) {
        video_data.preview_url := GetLargestPhotoUrl({"sizes": video.image})
    } else if (video.first_frame && video.first_frame.Length() > 0) {
        video_data.preview_url := GetLargestPhotoUrl({"sizes": video.first_frame})
    } else {
        video_data.preview_url := ""
    }
    
    return video_data
}

; Извлечение данных аудио
ExtractAudioData(audio) {
    audio_data := Object()
    audio_data.artist := audio.artist ? audio.artist : ""
    audio_data.title := audio.title ? audio.title : ""
    audio_data.duration := audio.duration ? audio.duration : 0
    audio_data.url := audio.url ? audio.url : ""
    
    return audio_data
}

; Извлечение данных документа
ExtractDocumentData(doc) {
    doc_data := Object()
    doc_data.id := doc.id
    doc_data.owner_id := doc.owner_id
    doc_data.title := doc.title ? doc.title : ""
    doc_data.ext := doc.ext ? doc.ext : ""
    doc_data.url := doc.url ? doc.url : ""
    doc_data.size := doc.size ? doc.size : 0
    doc_data.date := doc.date ? doc.date : 0
    doc_data.access_key := doc.access_key ? doc.access_key : ""
    
    ; Превью документа (если есть)
    if (doc.preview && doc.preview.photo && doc.preview.photo.sizes) {
        doc_data.preview_url := GetLargestPhotoUrl(doc.preview.photo)
    } else {
        doc_data.preview_url := ""
    }
    
    return doc_data
}

; Извлечение данных опроса
ExtractPollData(poll) {
    poll_data := Object()
    poll_data.id := poll.id
    poll_data.owner_id := poll.owner_id
    poll_data.question := poll.question ? poll.question : ""
    poll_data.votes := poll.votes ? poll.votes : 0
    poll_data.answers := []
    
    ; НОВЫЕ ПОЛЯ ДЛЯ ОПРОСА
    poll_data.anonymous := poll.anonymous ? poll.anonymous : 0
    poll_data.multiple := poll.multiple ? poll.multiple : 0
    poll_data.end_date := poll.end_date ? poll.end_date : 0
    poll_data.closed := poll.closed ? poll.closed : 0
    poll_data.is_board := poll.is_board ? poll.is_board : 0
    poll_data.can_edit := poll.can_edit ? poll.can_edit : 0
    poll_data.can_vote := poll.can_vote ? poll.can_vote : 0
    poll_data.can_report := poll.can_report ? poll.can_report : 0
    poll_data.can_share := poll.can_share ? poll.can_share : 0
    poll_data.author_id := poll.author_id ? poll.author_id : 0
    poll_data.background := poll.background ? poll.background : ""
    
    if (poll.answers && poll.answers.Length() > 0) {
        for index, answer in poll.answers {
            answer_data := Object()
            answer_data.id := answer.id
            answer_data.text := answer.text ? answer.text : ""
            answer_data.votes := answer.votes ? answer.votes : 0
            answer_data.rate := answer.rate ? answer.rate : 0
            poll_data.answers.Push(answer_data)
        }
    }
    
    return poll_data
}

; Извлечение данных репоста
ExtractRepostData(wall) {
    repost_data := Object()
    repost_data.id := wall.id
    repost_data.owner_id := wall.owner_id
    repost_data.from_id := wall.from_id
    repost_data.date := wall.date
    repost_data.text := wall.text ? wall.text : ""
    
    ; Статистика репоста
    if (wall.likes) {
        repost_data.likes := wall.likes.count
    }
    if (wall.reposts) {
        repost_data.reposts := wall.reposts.count
    }
    if (wall.comments) {
        repost_data.comments := wall.comments.count
    }
    if (wall.views) {
        repost_data.views := wall.views.count
    }
    
    return repost_data
}

; Извлечение данных альбома
ExtractAlbumData(album) {
    album_data := Object()
    album_data.id := album.id
    album_data.owner_id := album.owner_id
    album_data.title := album.title ? album.title : ""
    album_data.description := album.description ? album.description : ""
    album_data.size := album.size ? album.size : 0
    
    ; Обложка альбома
    if (album.thumb && album.thumb.sizes) {
        album_data.thumb_url := GetLargestPhotoUrl(album.thumb)
    } else {
        album_data.thumb_url := ""
    }
    
    return album_data
}

2

Re: AHK v1: Обновить состояние скрипта, скачивающего посты ВК

Только сейчас увидел, что надублировал тем. Извиняюсь. Связано это с тем, что после каждой попытки опубликовать тему, всплывало сообщение об ошибке и предложение попробовать через 5-10 минут. Видимо, из-за большого размера скрипта.

3 (изменено: 1srafel, 2025-10-23 06:37:19)

Re: AHK v1: Обновить состояние скрипта, скачивающего посты ВК

AHK v1
(Вторая часть кода)

; Получение самой большой фотографии
GetLargestPhotoUrl(photo) {
    if (!photo.sizes || photo.sizes.Length() = 0) {
        return ""
    }
    
    ; Сортируем размеры по ширине (от большего к меньшему)
    largest_size := ""
    largest_width := 0
    
    for index, size in photo.sizes {
        if (size.width > largest_width) {
            largest_width := size.width
            largest_size := size
        }
    }
    
    return largest_size ? largest_size.url : ""
}

; Конвертация поста в HTML
ConvertPostToHTML(post) {
    html := ""
    html .= "<!DOCTYPE html>`n"
    html .= "<html lang='ru'>`n"
    html .= "<head>`n"
    html .= "    <meta charset='UTF-8'>`n"
    html .= "    <meta name='viewport' content='width=device-width, initial-scale=1.0'>`n"
    
    ; 1) Используем title из JSON как заголовок поста
    post_title := post.title ? EscapeHTML(post.title) : "Пост " post.id " из " post.owner_id
    html .= "    <title>" post_title "</title>`n"
    
    html .= "    <style>`n"
    html .= "        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; }`n"
    html .= "        .post { background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }`n"
    html .= "        .post-header { display: flex; align-items: center; justify-content: space-between; color: #666; font-size: 14px; margin-bottom: 15px; }`n"
    html .= "        .post-header-info { display: flex; align-items: center; flex-grow: 1; }`n"
    html .= "        .avatar { width: 40px; height: 40px; border-radius: 50%; margin-right: 10px; vertical-align: middle; border: 1px solid #e1e3e6; }`n"
    html .= "        .date { color: #2a5885; text-decoration: none; font-weight: bold; }`n"
    html .= "        .date:hover { text-decoration: underline; }`n"
    html .= "        .post-title { font-size: 20px; font-weight: bold; margin-bottom: 15px; color: #000; border-bottom: 2px solid #2a5885; padding-bottom: 10px; }`n"
    html .= "        .media-attachments { margin-bottom: 15px; }`n"
    html .= "        .media-attachment { margin-bottom: 10px; }`n"
    html .= "        .my { max-width: 100%; height: auto; border-radius: 8px; display: block; }`n"
    html .= "        .post-text { font-size: 16px; line-height: 1.5; margin-bottom: 15px; white-space: pre-wrap; }`n"
    html .= "        .post-stats { color: #666; font-size: 14px; margin-bottom: 15px; padding-top: 15px; border-top: 1px solid #eee; }`n"
    html .= "        .group-name { color: #2a5885; font-weight: bold; }`n"
    html .= "        .link-attachment { border: 1px solid #e1e3e6; border-radius: 8px; padding: 12px; margin-bottom: 10px; display: flex; align-items: center; }`n"
    html .= "        .link-preview { width: 80px; height: 80px; border-radius: 4px; margin-right: 12px; object-fit: cover; }`n"
    html .= "        .link-info { flex: 1; }`n"
    html .= "        .link-title { font-weight: bold; margin-bottom: 4px; }`n"
    html .= "        .link-description { color: #666; font-size: 14px; margin-bottom: 4px; }`n"
    html .= "        .link-url { color: #2a5885; font-size: 12px; }`n"
    html .= "        .section-title { font-size: 16px; font-weight: bold; margin: 15px 0 10px 0; color: #aaaaaa; }`n"
    html .= "        .media-info { color: #666; font-size: 14px; margin-top: 5px; }`n"
    html .= "        .document-attachment { border: 1px solid #e1e3e6; border-radius: 8px; padding: 12px; margin-bottom: 10px; display: flex; align-items: center; }`n"
    html .= "        .document-icon { width: 48px; height: 48px; margin-right: 12px; background: #f0f0f0; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 24px; }`n"
    html .= "        .document-info { flex: 1; }`n"
    html .= "        .document-title { font-weight: bold; margin-bottom: 4px; }`n"
    html .= "        .document-details { color: #666; font-size: 12px; }`n"
    html .= "        .poll-attachment { border: 1px solid #e1e3e6; border-radius: 8px; padding: 12px; margin-bottom: 10px; }`n"
    html .= "        .poll-question { font-weight: bold; margin-bottom: 10px; }`n"
    html .= "        .poll-answer { margin-bottom: 5px; padding: 5px; background: #f9f9f9; border-radius: 4px; }`n"
    html .= "        .poll-votes { color: #666; font-size: 12px; margin-top: 5px; }`n"
    html .= "        .poll-properties { margin-bottom: 10px; padding: 8px; background: #f0f8ff; border-radius: 6px; font-size: 12px; }`n"
    html .= "        .poll-property { display: inline-block; margin-right: 10px; margin-bottom: 5px; padding: 3px 8px; background: white; border-radius: 12px; border: 1px solid #d4e6ff; }`n"
    html .= "        .poll-stats { color: #666; font-size: 12px; margin-top: 8px; }`n"
    html .= "        .repost-attachment { border: 1px solid #e1e3e6; border-radius: 8px; padding: 12px; margin-bottom: 10px; background: #f9f9f9; }`n"
    html .= "        .album-attachment { border: 1px solid #e1e3e6; border-radius: 8px; padding: 12px; margin-bottom: 10px; display: flex; align-items: center; }`n"
    html .= "        .album-icon { width: 48px; height: 48px; margin-right: 12px; background: #f0f0f0; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 24px; }`n"
    html .= "        .album-info { flex: 1; }`n"
    html .= "        .album-title { font-weight: bold; margin-bottom: 4px; }`n"
    html .= "        .album-details { color: #666; font-size: 12px; }`n"
    html .= "        a { color: #2a5885; text-decoration: none; }`n"
    html .= "        a:hover { text-decoration: underline; }`n"
    html .= "        .text-link { color: #2a5885; text-decoration: none; }`n"
    html .= "        .text-link:hover { text-decoration: underline; }`n"
    html .= "        .mention { color: #2a5885; text-decoration: none; }`n"
    html .= "        .mention:hover { text-decoration: underline; }`n"
    html .= "    </style>`n"
    html .= "</head>`n"
    html .= "<body>`n"
    
    ; Пост
    html .= "<div class='post'>`n"
    
    ; 1) Заголовок поста (если есть в JSON) - содержимое "title" попадает сюда
    if (post.title != "") {
        html .= "    <h1 class='post-title'>" EscapeHTML(post.title) "</h1>`n"
    }
    
    ; Заголовок поста с информацией о группе
    html .= "    <div class='post-header'>`n"
    
    ; Определяем аватар (группы или пользователя)
    avatar_url := ""
    if (post.group_avatar != "") {
        avatar_url := post.group_avatar
    } else if (post.user_avatar != "") {
        avatar_url := post.user_avatar
    }
    
    ; Отображаем аватар и информацию
    html .= "        <div class='post-header-info'>`n"
    if (avatar_url != "") {
        html .= "            <img class='avatar' src='" avatar_url "' alt='Аватар'>`n"
    }
    
    ; 2) Название группы оборачиваем в ссылку из screen_name
    if (post.group_screen_name != "") {
        html .= "            <a class='group-name' href='https://vk.com/" post.group_screen_name "' target='_blank'>" EscapeHTML(post.group_name) "</a>`n"
    } else {
        html .= "            <span class='group-name'>" EscapeHTML(post.group_name) "</span>`n"
    }
    html .= "        </div>`n"
    
    html .= "        <a class='date' href='https://vk.com/wall" post.owner_id "_" post.id "' target='_blank'>" FormatTimestampHTML(post.date) "</a>`n"
    html .= "    </div>`n"
    
    ; Фото
    if (post.photos.MaxIndex() > 0) {
        html .= "    <div class='media-attachments'>`n"
        for index, photo_url in post.photos {
            html .= "        <div class='media-attachment'>`n"
            html .= "            <a href='" photo_url "' target='_blank'><img class='my' src='" photo_url "' alt='Фото'></a>`n"
            html .= "        </div>`n"
        }
        html .= "    </div>`n"
    }
    
    ; Ссылки
    if (post.links.MaxIndex() > 0) {
        html .= "    <div class='media-attachments'>`n"
        for index, link_data in post.links {
            html .= "        <div class='link-attachment'>`n"
            if (link_data.preview_url != "") {
                html .= "            <img class='link-preview' src='" link_data.preview_url "' alt='Превью'>`n"
            }
            html .= "            <div class='link-info'>`n"
            if (link_data.title != "") {
                html .= "                <div class='link-title'><a href='" link_data.url "' target='_blank'>" EscapeHTML(link_data.title) "</a></div>`n"
            }
            if (link_data.description != "") {
                html .= "                <div class='link-description'>" EscapeHTML(link_data.description) "</div>`n"
            }
            html .= "                <div class='link-url'>" link_data.url "</div>`n"
            html .= "            </div>`n"
            html .= "        </div>`n"
        }
        html .= "    </div>`n"
    }
    
    ; Видео
    if (post.videos.MaxIndex() > 0) {
        html .= "    <div class='media-attachments'>`n"
        html .= "        <div class='section-title'>[Видео: " post.videos.MaxIndex() "]</div>`n"
        for index, video_data in post.videos {
            html .= "        <div class='media-attachment'>`n"
            if (video_data.preview_url != "") {
                html .= "            <a href='https://vk.com/video" video_data.owner_id "_" video_data.id "' target='_blank'><img class='my' src='" video_data.preview_url "' alt='Видео'></a>`n"
            }
            html .= "            <div class='media-info'>`n"
            if (video_data.duration > 0) {
                html .= "                Длительность: " FormatDuration(video_data.duration) "`n"
            }
            html .= "            </div>`n"
            html .= "            <h3>" EscapeHTML(video_data.title) "</h3>`n"
            html .= "        </div>`n"
        }
        html .= "    </div>`n"
    }
    
    ; Аудио
    if (post.audios.MaxIndex() > 0) {
        html .= "    <div class='media-attachments'>`n"
        html .= "        <div class='section-title'>[Аудио: " post.audios.MaxIndex() "]</div>`n"
        for index, audio_data in post.audios {
            html .= "        <div class='media-attachment'>`n"
            html .= "            <div><strong>" EscapeHTML(audio_data.artist) " - " EscapeHTML(audio_data.title) "</strong></div>`n"
            html .= "            <div class='media-info'>Длительность: " FormatDuration(audio_data.duration) "</div>`n"
            html .= "        </div>`n"
        }
        html .= "    </div>`n"
    }

4 (изменено: 1srafel, 2025-10-23 06:45:02)

Re: AHK v1: Обновить состояние скрипта, скачивающего посты ВК

AHK v1
(Третья часть кода)

    ; Документы
    if (post.documents.MaxIndex() > 0) {
        html .= "    <div class='media-attachments'>`n"
        html .= "        <div class='section-title'>[Документы: " post.documents.MaxIndex() "]</div>`n"
        for index, doc_data in post.documents {
            html .= "        <div class='document-attachment'>`n"
            html .= "            <div class='document-icon'>

5

Re: AHK v1: Обновить состояние скрипта, скачивающего посты ВК

Форум не дает сохранить 3-ю часть.

6

Re: AHK v1: Обновить состояние скрипта, скачивающего посты ВК

1srafel пишет:

Форум не дает сохранить 3-ю часть.

Тестовый код:

#Requires AutoHotkey v2

part1 := [
    {note: 'D' , octave: 5, start:    0, duration: 200},
    {note: 'D' , octave: 5, start:  600, duration: 200},
    {note: 'B' , octave: 4, start: 1200, duration: 400},
    {note: 'A#', octave: 4, start: 1600, duration: 200},
    {note: 'B' , octave: 4, start: 1800, duration: 400},
    {note: 'C#', octave: 5, start: 2200, duration: 200},

    {note: 'D' , octave: 4, start:    0, duration: 350},
    {note: 'F#', octave: 4, start:    0, duration: 350},
    {note: 'G' , octave: 4, start: 1200, duration: 400},
    {note: 'D' , octave: 4, start: 1200, duration: 400},
    {note: 'F#', octave: 4, start: 1600, duration: 200},
    {note: 'G' , octave: 4, start: 1800, duration: 400},
    {note: 'A' , octave: 4, start: 2200, duration: 200},
]

part2 := [
    {note: 'D' , octave: 5, start:    0, duration: 200},
    {note: 'A' , octave: 4, start:    0, duration: 200},
    {note: 'E' , octave: 5, start:  600, duration: 200},
    {note: 'C#', octave: 5, start:  600, duration: 200},
    {note: 'F#', octave: 5, start: 1200, duration: 200},
    {note: 'F#', octave: 5, start: 1800, duration: 200},
    {note: 'D' , octave: 5, start: 1800, duration: 200},

    {note: 'D' , octave: 4, start:    0, duration: 350},
    {note: 'F#', octave: 4, start:    0, duration: 350},
    {note: 'C#', octave: 5, start:  600, duration: 200},
    {note: 'D' , octave: 5, start: 1200, duration: 200}
]

part3 := [
    {note: 'G' , octave: 5, start:    0, duration: 400},
    {note: 'E' , octave: 5, start:    0, duration: 400},
    {note: 'F#', octave: 5, start:  400, duration: 200},
    {note: 'D' , octave: 5, start:  400, duration: 200},
    {note: 'E' , octave: 5, start:  600, duration: 400},
    {note: 'C#', octave: 5, start:  600, duration: 400},
    {note: 'D' , octave: 5, start: 1000, duration: 200},
    {note: 'B' , octave: 4, start: 1000, duration: 200},
    {note: 'E' , octave: 5, start: 1200, duration: 200},
    {note: 'C#', octave: 5, start: 1200, duration: 200},
    {note: 'A' , octave: 4, start: 1550, duration: 100},
    {note: 'A' , octave: 4, start: 1670, duration: 100},
    {note: 'A' , octave: 4, start: 1800, duration: 200},
    {note: 'A' , octave: 4, start: 2100, duration: 200},

    {note: 'D' , octave: 4, start:    0, duration: 990},
    {note: 'A' , octave: 4, start:    0, duration: 990},
    {note: 'A' , octave: 3, start: 1200, duration: 400}
]

part4 := [
    {note: 'E' , octave: 5, start:    0, duration: 400},
    {note: 'C#', octave: 5, start:    0, duration: 400},
    {note: 'D',  octave: 5, start:  400, duration: 200},
    {note: 'B',  octave: 4, start:  400, duration: 200},
    {note: 'C#', octave: 4, start:  600, duration: 400},
    {note: 'A',  octave: 4, start:  600, duration: 400},
    {note: 'B' , octave: 4, start: 1000, duration: 200},
    {note: 'G#', octave: 4, start: 1000, duration: 200},
    {note: 'A' , octave: 4, start: 1200, duration: 200},

    {note: 'E' , octave: 4, start:    0, duration: 990},
    {note: 'A' , octave: 3, start: 1200, duration: 200},
    {note: 'A' , octave: 3, start: 1550, duration: 100},
    {note: 'A' , octave: 3, start: 1670, duration: 100},
    {note: 'A' , octave: 3, start: 1800, duration: 200},
    {note: 'A' , octave: 3, start: 2100, duration: 200}
]

part5 := [
    {note: 'A' , octave: 4, start:    0, duration: 200},
    {note: 'E' , octave: 4, start:    0, duration: 200},
    {note: 'A' , octave: 4, start:  600, duration: 200},
    {note: 'Bb', octave: 4, start: 1200, duration: 400},
    {note: 'A' , octave: 4, start: 1600, duration: 200},
    {note: 'Bb', octave: 4, start: 1800, duration: 400},
    {note: 'G#', octave: 4, start: 2200, duration: 200},

    {note: 'C#', octave: 4, start:    0, duration: 200},
    {note: 'A' , octave: 3, start:    0, duration: 200},
    {note: 'F#', octave: 3, start: 1200, duration: 990},
    {note: 'D' , octave: 3, start: 1200, duration: 990}
]

part6 := [
    {note: 'A' , octave: 4, start:    0, duration: 200},
    {note: 'B' , octave: 4, start:  600, duration: 200},
    {note: 'G#', octave: 4, start: 1200, duration: 200},
    {note: 'C#', octave: 5, start: 1200, duration: 200},
    {note: 'A' , octave: 4, start: 1800, duration: 200},
    {note: 'C#', octave: 5, start: 1800, duration: 200},

    {note: 'C#', octave: 4, start:    0, duration: 200},
    {note: 'A' , octave: 3, start:    0, duration: 200},
    {note: 'E' , octave: 4, start:  600, duration: 200},
    {note: 'G#', octave: 4, start:  600, duration: 200},
    {note: 'C#', octave: 4, start: 1200, duration: 200},
    {note: 'F' , octave: 4, start: 1200, duration: 200},
    {note: 'F#', octave: 4, start: 1800, duration: 200}
]

part7 := [
    {note: 'B' , octave: 4, start:    0, duration: 400},
    {note: 'D' , octave: 5, start:    0, duration: 400},
    {note: 'C#', octave: 5, start:  400, duration: 200},
    {note: 'B' , octave: 4, start:  600, duration: 400},
    {note: 'A' , octave: 4, start: 1000, duration: 200},
    {note: 'G#', octave: 4, start: 1200, duration: 400},

    {note: 'B' , octave: 3, start:    0, duration: 400},
    {note: 'F#', octave: 4, start:    0, duration: 400},
    {note: 'E' , octave: 4, start: 1200, duration: 400},
    {note: 'D' , octave: 4, start: 1600, duration: 200},
    {note: 'C#', octave: 4, start: 1800, duration: 400},
    {note: 'B' , octave: 3, start: 2200, duration: 200},
]

part8 := [
    {note: 'B' , octave: 4, start:    0, duration: 400},
    {note: 'D' , octave: 5, start:    0, duration: 400},
    {note: 'C#', octave: 5, start:  400, duration: 200},
    {note: 'B' , octave: 4, start:  600, duration: 400},
    {note: 'C#', octave: 5, start: 1000, duration: 200},
    {note: 'A' , octave: 4, start: 1200, duration: 400},

    {note: 'B' , octave: 3, start:    0, duration: 200},
    {note: 'F#', octave: 4, start:    0, duration: 200},
    {note: 'E' , octave: 4, start:  600, duration: 200},
    {note: 'G#', octave: 4, start:  600, duration: 200},
    {note: 'A' , octave: 3, start: 1200, duration: 400},
    {note: 'A' , octave: 3, start: 1600, duration: 200},
    {note: 'B' , octave: 3, start: 1800, duration: 400},
    {note: 'C#', octave: 4, start: 2200, duration: 200},
]

part9 := [
    {note: 'E' , octave: 5, start:    0, duration: 400},
    {note: 'D#', octave: 5, start:  400, duration: 200},
    {note: 'E' , octave: 5, start:  600, duration: 400},
    {note: 'F#', octave: 5, start: 1000, duration: 200},
    {note: 'D' , octave: 5, start: 1200, duration: 400},

    {note: 'G' , octave: 4, start:    0, duration: 200},
    {note: 'B' , octave: 4, start:    0, duration: 200},
    {note: 'A' , octave: 4, start:  600, duration: 200},
    {note: 'C#', octave: 5, start:  600, duration: 200},
    {note: 'D' , octave: 4, start: 1200, duration: 400}
]

parts := [
    '1', '1', '2', '3', '1', '1', '2', '4',
    '5', '5', '6', '7', '5', '5', '6', '8',
    '1', '1', '2', '3', '1', '1', '2', '9'
]

notes := []
for i, n in parts {
    AddPart(notes, part%n%, 2400, i - 1)
}

PlayNotes(notes, 0x7000)

AddPart(notes, part, len, offset) {
    for item in part {
        newItem := item.Clone()
        newItem.start += len * offset
        notes.Push(newItem)
    }
}

ShowGui() {
    wnd := Gui('AlwaysOnTop -Caption Owner')
    wnd.BackColor := 0xC8BA8D
    wnd.MarginX := wnd.MarginY := 25
    wnd.SetFont('s18 c0x6A688C', 'Calibri')
    wnd.AddText(, 'The buffer is being formed, this may take a few seconds...')
    wnd.Show()
    return wnd
}

PlayNotes(notes, amp := 0x7FFF, attackMs := 5, decayMs := 30,
          sustainLevel := 0.7, releaseMs := 100, sampleRate := 48000) {
    wnd := ShowGui()
    buf := GeneratePolyphonicBuffer(notes, sampleRate, amp, attackMs, decayMs, sustainLevel, releaseMs)
    wnd.Destroy()
    PlayBuffer(buf, sampleRate)
}

CreateNoteMap(startOctave := 0, endOctave := 8) {
    noteMap := Map()
    notes := ['C', ['C#','Db'], 'D', ['D#','Eb'], 'E', 'F', 
              ['F#','Gb'], 'G', ['G#','Ab'], 'A', ['A#','Bb'], 'B']
    
    A4_freq := 440
    A4_semitone := 57  ; A4 = 4×12 + 9
    
    Loop (endOctave - startOctave + 1) {
        octave := startOctave + A_Index - 1
        Loop 12 {
            noteIndex := A_Index - 1
            semitone := octave * 12 + noteIndex
            freq := A4_freq * (2 ** ((semitone - A4_semitone) / 12))
            
            note := notes[noteIndex + 1]
            if note is Array {
                noteMap[note[1] . octave] := freq  ; sharp
                noteMap[note[2] . octave] := freq  ; flat
            } else {
                noteMap[note . octave] := freq
            }
        }
    }
    return noteMap
}

GeneratePolyphonicBuffer(notes, sampleRate := 48000, amp := 20000, 
                         attackMs := 10, decayMs := 50, sustainLevel := 0.7, releaseMs := 100) {
    static noteMap := CreateNoteMap(0, 8), twoPi := 2 * ACos(-1)
    
    totalDuration := 0
    for note in notes {
        endTime := note.start + note.duration
        if endTime > totalDuration {
            totalDuration := endTime
        }
    }
    
    if totalDuration = 0 {
        return Buffer(0)
    }
    
    totalSamples := Round(sampleRate * totalDuration / 1000)
    floatBuf := Buffer(totalSamples * 8, 0)
    attackSamples  := Round(sampleRate * attackMs  / 1000)
    decaySamples   := Round(sampleRate * decayMs   / 1000)
    releaseSamples := Round(sampleRate * releaseMs / 1000)
    
    for note in notes {
        noteName := note.note . note.octave
        if !noteMap.Has(noteName) {
            throw Error('Unknown note: ' . noteName)
        }
        
        freq := noteMap[noteName]
        startSample := Round(sampleRate * note.start / 1000)
        noteSamples := Round(sampleRate * note.duration / 1000)
        
        phaseInc := twoPi * freq / sampleRate
        phase := 0.0
        
        Loop noteSamples {
            i := A_Index - 1
            globalIndex := startSample + i
            
            if globalIndex >= totalSamples {
                break
            }
            
            sample := Sin(phase) * amp
            phase += phaseInc
            
            if i < attackSamples {
                sample *= i / attackSamples
            } else if i < attackSamples + decaySamples {
                decayProgress := (i - attackSamples) / decaySamples
                sample *= 1 - (1 - sustainLevel) * decayProgress
            } else if i < noteSamples - releaseSamples {
                sample *= sustainLevel
            } else {
                releaseProgress := (i - (noteSamples - releaseSamples)) / releaseSamples
                sample *= sustainLevel * (1 - releaseProgress)
            }
            
            offset := globalIndex * 8
            currentVal := NumGet(floatBuf, offset, 'Double')
            NumPut('Double', currentVal + sample, floatBuf, offset)
        }
    }
    
    ; normalization
    maxAmp := 0.0
    Loop totalSamples {
        absVal := Abs(NumGet(floatBuf, (A_Index - 1) * 8, 'Double'))
        if absVal > maxAmp {
            maxAmp := absVal
        }
    }
    
    scale := (maxAmp > 32767) ? (32767 / maxAmp) : 1.0
    
    ; converting to int16 PCM
    buf := Buffer(totalSamples * 2, 0)
    Loop totalSamples {
        value := Round(NumGet(floatBuf, (A_Index - 1) * 8, 'Double') * scale)
        NumPut('Short', value, buf, (A_Index - 1) * 2)
    }
    
    return buf
}

PlayBuffer(buf, sampleRate := 48000, channels := 1) {
    static WAVE_FORMAT_PCM := 1, WHDR_DONE := 0x00000001, bytesPerSample := 2
    if buf.Size = 0 {
        return
    }
    blockAlign := channels * bytesPerSample
    avgBytesPerSec := sampleRate * blockAlign

    WAVEFORMATEX := Buffer(18, 0)
    NumPut('UShort', WAVE_FORMAT_PCM, 'UShort', channels,
           'UInt'  , sampleRate     , 'UInt'  , avgBytesPerSec,
           'UShort', blockAlign     , 'UShort', bytesPerSample * 8, WAVEFORMATEX)
    r := DllCall('winmm\waveOutOpen', 'Ptr*', &hWaveOut := 0, 'UInt', 0xFFFFFFFF, 'Ptr', WAVEFORMATEX, 'Ptr', 0, 'Ptr', 0, 'UInt', 0)
    if r != 0 {
        throw Error('waveOutOpen failed: ' . r)
    }

    WAVEHDR := Buffer(A_PtrSize = 8 ? 48 : 32, 0)
    NumPut('Ptr', buf.ptr, 'UInt', buf.size, WAVEHDR)
    DllCall('winmm\waveOutPrepareHeader', 'Ptr', hWaveOut, 'Ptr', WAVEHDR, 'UInt', WAVEHDR.size)
    DllCall('winmm\waveOutWrite', 'Ptr', hWaveOut, 'Ptr', WAVEHDR, 'UInt', WAVEHDR.size)

    Loop {
        Sleep 100
    } until NumGet(WAVEHDR, A_PtrSize * 2 + 8, 'UInt') & WHDR_DONE

    DllCall('winmm\waveOutUnprepareHeader', 'Ptr', hWaveOut, 'Ptr', WAVEHDR, 'UInt', WAVEHDR.size)
    DllCall('winmm\waveOutClose', 'Ptr', hWaveOut)
}

У меня всё вроде работает, никаких проблем.

1srafel пишет:

Для запроса используется приведенная "хранимая процедура", которую нужно создать на сайте - и токен

Да как-то слишком заморочно ради одного ответа на форуме. Я бы платно взялся.

Разработка AHK-скриптов:
e-mail dfiveg@mail.ru
Telegram jollycoder

7 (изменено: 1srafel, 2025-10-23 22:15:58)

Re: AHK v1: Обновить состояние скрипта, скачивающего посты ВК

У меня каждый раз сообщение:

Извините! Произошла ошибка.
Это временная ошибка. Просто обновите страницу. Если проблема не решается, попробуйте повторить через 5-10 минут.

teadrinker пишет:

Да как-то слишком заморочно ради одного ответа на форуме.

Сам код с процедурой и токеном вполне рабочий, если сделать 2 скрипта - где один запускает второй и таким образом сбрасывает какие-то данные, мешающие очередной итерации. Хотелось бы понять, в чем теоретически может быть проблема. То есть, нужно как-то добиться, чтобы на второй итерации скрипт полностью обнулялся, как будто его только что запустили и в этом случае вторая итерация работает.